diff --git a/CHANGELOG.md b/CHANGELOG.md index 71ace1fde..bb3c238a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.1...dev) +### Added +- Manual email verification requests. ([bc659387](https://github.com/pixelfed/pixelfed/commit/bc659387)) + ### Updated - Updated NotificationService, fix 500 bug. ([4a609dc3](https://github.com/pixelfed/pixelfed/commit/4a609dc3)) - Updated HttpSignatures, update instance actor headers. Fixes #2935. ([a900de21](https://github.com/pixelfed/pixelfed/commit/a900de21)) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 1103900fa..edbf6a4cc 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -75,7 +75,10 @@ class AccountController extends Controller public function verifyEmail(Request $request) { - return view('account.verify_email'); + $recentSent = EmailVerification::whereUserId(Auth::id()) + ->whereDate('created_at', '>', now()->subHours(12))->count(); + + return view('account.verify_email', compact('recentSent')); } public function sendVerifyEmail(Request $request) diff --git a/app/Http/Controllers/Admin/AdminReportController.php b/app/Http/Controllers/Admin/AdminReportController.php index 2af1b2bee..d2625acff 100644 --- a/app/Http/Controllers/Admin/AdminReportController.php +++ b/app/Http/Controllers/Admin/AdminReportController.php @@ -4,8 +4,12 @@ namespace App\Http\Controllers\Admin; use Cache; use App\Report; +use App\User; use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Redis; +use App\Services\AccountService; +use App\Services\StatusService; trait AdminReportController { @@ -33,6 +37,7 @@ trait AdminReportController $report = Report::findOrFail($id); $this->handleReportAction($report, $action); + Cache::forget('admin-dash:reports:list-cache'); return response()->json(['msg'=> 'Success']); } @@ -52,17 +57,20 @@ trait AdminReportController $item->is_nsfw = true; $item->save(); $report->nsfw = true; + StatusService::del($item->id); break; case 'unlist': $item->visibility = 'unlisted'; $item->save(); Cache::forget('profiles:private'); + StatusService::del($item->id); break; case 'delete': // Todo: fire delete job $report->admin_seen = null; + StatusService::del($item->id); break; case 'shadowban': @@ -115,4 +123,55 @@ trait AdminReportController ]; return response()->json($res); } + + public function reportMailVerifications(Request $request) + { + $ids = Redis::smembers('email:manual'); + $ignored = Redis::smembers('email:manual-ignored'); + $reports = []; + if($ids) { + $reports = collect($ids) + ->filter(function($id) use($ignored) { + return !in_array($id, $ignored); + }) + ->map(function($id) { + $account = AccountService::get($id); + $user = User::whereProfileId($id)->first(); + if(!$user) { + return []; + } + $account['email'] = $user->email; + return $account; + }) + ->filter(function($res) { + return isset($res['id']); + }) + ->values(); + } + return view('admin.reports.mail_verification', compact('reports', 'ignored')); + } + + public function reportMailVerifyIgnore(Request $request) + { + $id = $request->input('id'); + Redis::sadd('email:manual-ignored', $id); + return redirect('/i/admin/reports'); + } + + public function reportMailVerifyApprove(Request $request) + { + $id = $request->input('id'); + $user = User::whereProfileId($id)->firstOrFail(); + Redis::srem('email:manual', $id); + Redis::srem('email:manual-ignored', $id); + $user->email_verified_at = now(); + $user->save(); + return redirect('/i/admin/reports'); + } + + public function reportMailVerifyClearIgnored(Request $request) + { + Redis::del('email:manual-ignored'); + return [200]; + } } diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 8c9d7e218..f31bc823a 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -17,6 +17,7 @@ use App\{ use DB, Cache; use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Redis; use App\Http\Controllers\Admin\{ AdminDiscoverController, AdminInstanceController, @@ -28,12 +29,13 @@ use App\Http\Controllers\Admin\{ }; use Illuminate\Validation\Rule; use App\Services\AdminStatsService; +use App\Services\StatusService; use App\Services\StoryService; class AdminController extends Controller { use AdminReportController, - AdminDiscoverController, + AdminDiscoverController, AdminMediaController, AdminSettingsController, AdminInstanceController, @@ -54,9 +56,15 @@ class AdminController extends Controller public function statuses(Request $request) { - $statuses = Status::orderBy('id', 'desc')->simplePaginate(10); - - return view('admin.statuses.home', compact('statuses')); + $statuses = Status::orderBy('id', 'desc')->cursorPaginate(10); + $data = $statuses->map(function($status) { + return StatusService::get($status->id, false); + }) + ->filter(function($s) { + return $s; + }) + ->toArray(); + return view('admin.statuses.home', compact('statuses', 'data')); } public function showStatus(Request $request, $id) @@ -69,17 +77,45 @@ class AdminController extends Controller public function reports(Request $request) { $filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; - $reports = Report::whereHas('status') - ->whereHas('reportedUser') - ->whereHas('reporter') - ->orderBy('created_at','desc') - ->when($filter, function($q, $filter) { - return $filter == 'open' ? - $q->whereNull('admin_seen') : - $q->whereNotNull('admin_seen'); - }) - ->paginate(6); - return view('admin.reports.home', compact('reports')); + $page = $request->input('page') ?? 1; + + $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() { + return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count(); + }); + + $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() { + return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count(); + }); + + $mailVerifications = Redis::scard('email:manual'); + + if($filter == 'open' && $page == 1) { + $reports = Cache::remember('admin-dash:reports:list-cache', 300, function() use($page, $filter) { + return Report::whereHas('status') + ->whereHas('reportedUser') + ->whereHas('reporter') + ->orderBy('created_at','desc') + ->when($filter, function($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->paginate(6); + }); + } else { + $reports = Report::whereHas('status') + ->whereHas('reportedUser') + ->whereHas('reporter') + ->orderBy('created_at','desc') + ->when($filter, function($q, $filter) { + return $filter == 'open' ? + $q->whereNull('admin_seen') : + $q->whereNotNull('admin_seen'); + }) + ->paginate(6); + } + + return view('admin.reports.home', compact('reports', 'ai', 'spam', 'mailVerifications')); } public function showReport(Request $request, $id) @@ -143,7 +179,7 @@ class AdminController extends Controller Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); - + Cache::forget('admin-dash:reports:spam-count'); return redirect('/i/admin/reports/autospam'); } @@ -156,8 +192,11 @@ class AdminController extends Controller $appeal->appeal_handled_at = now(); $appeal->save(); + StatusService::del($status->id); + Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id); Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id); + Cache::forget('admin-dash:reports:spam-count'); return redirect('/i/admin/reports/autospam'); } @@ -176,7 +215,7 @@ class AdminController extends Controller if($action == 'dismiss') { $appeal->appeal_handled_at = now(); $appeal->save(); - + Cache::forget('admin-dash:reports:ai-count'); return redirect('/i/admin/reports/appeals'); } @@ -201,6 +240,8 @@ class AdminController extends Controller $appeal->appeal_handled_at = now(); $appeal->save(); + StatusService::del($status->id); + Cache::forget('admin-dash:reports:ai-count'); return redirect('/i/admin/reports/appeals'); } diff --git a/app/Http/Controllers/InternalApiController.php b/app/Http/Controllers/InternalApiController.php index 23bb687ba..b9f86a639 100644 --- a/app/Http/Controllers/InternalApiController.php +++ b/app/Http/Controllers/InternalApiController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use App\{ AccountInterstitial, + Bookmark, DirectMessage, DiscoverCategory, Hashtag, @@ -19,6 +20,7 @@ use App\{ UserFilter, }; use Auth,Cache; +use Illuminate\Support\Facades\Redis; use Carbon\Carbon; use League\Fractal; use App\Transformer\Api\{ @@ -345,14 +347,18 @@ class InternalApiController extends Controller public function bookmarks(Request $request) { - $statuses = Auth::user()->profile - ->bookmarks() - ->withCount(['likes','comments']) - ->orderBy('created_at', 'desc') - ->simplePaginate(10); - - $resource = new Fractal\Resource\Collection($statuses, new StatusTransformer()); - $res = $this->fractal->createData($resource)->toArray(); + $res = Bookmark::whereProfileId($request->user()->profile_id) + ->orderByDesc('created_at') + ->simplePaginate(10) + ->map(function($bookmark) { + $status = StatusService::get($bookmark->status_id); + $status['bookmarked_at'] = $bookmark->created_at->format('c'); + return $status; + }) + ->filter(function($bookmark) { + return isset($bookmark['id']); + }) + ->values(); return response()->json($res); } @@ -456,4 +462,18 @@ class InternalApiController extends Controller $template = $status->in_reply_to_id ? 'status.reply' : 'status.remote'; return view($template, compact('user', 'status')); } + + public function requestEmailVerification(Request $request) + { + $pid = $request->user()->profile_id; + $exists = Redis::sismember('email:manual', $pid); + return view('account.email.request_verification', compact('exists')); + } + + public function requestEmailVerificationStore(Request $request) + { + $pid = $request->user()->profile_id; + Redis::sadd('email:manual', $pid); + return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']); + } } diff --git a/app/Http/Middleware/EmailVerificationCheck.php b/app/Http/Middleware/EmailVerificationCheck.php index 1dbb17e90..e3d278c34 100644 --- a/app/Http/Middleware/EmailVerificationCheck.php +++ b/app/Http/Middleware/EmailVerificationCheck.php @@ -21,7 +21,7 @@ class EmailVerificationCheck is_null($request->user()->email_verified_at) && !$request->is( 'i/auth/*', - 'i/verify-email', + 'i/verify-email*', 'log*', 'site*', 'i/confirm-email/*', diff --git a/resources/views/account/email/request_verification.blade.php b/resources/views/account/email/request_verification.blade.php new file mode 100644 index 000000000..a4d641597 --- /dev/null +++ b/resources/views/account/email/request_verification.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.app') + +@section('content') +
+
+ @if (session('status')) +
+

{{ session('status') }}

+
+ @endif + @if (session('error')) +
+

{{ session('error') }}

+
+ @endif +
+
Request Manual Email Verification
+
+

If you are experiencing issues receiving your email address confirmation code to the email address you registered with, you can request manual verification as a last resort. An administrator will review your request.

+ +

If you request manual email verification, you still may experience issues recieving emails from our service, including password reset requests.

+ +

In the event you need to reset your password and do not receive the password reset email, please contact the administrators.

+ + @if(!$exists) +
+ @csrf + +
+ @else + + @endif +
+
+
+
+@endsection diff --git a/resources/views/account/verify_email.blade.php b/resources/views/account/verify_email.blade.php index 3286a1e84..4ed3997d2 100644 --- a/resources/views/account/verify_email.blade.php +++ b/resources/views/account/verify_email.blade.php @@ -13,19 +13,35 @@

{{ session('error') }}

@endif + + @if(Auth::user()->email_verified_at) +

Your email is already verified. Click here to go home.

+ @else
Confirm Email Address
-

You need to confirm your email address ({{Auth::user()->email}}) before you can proceed.

-

You can change your email address here.

-

If you don't recieve an email within 30 minutes, you can contact the administrator.

-
+

You need to confirm your email address {{Auth::user()->email}} before you can proceed.

+ @if(!$recentSent)
@csrf
+ @else + + @endif +

Click here to change your email address.

+ + @if($recentSent) +
+
+

If you are experiencing issues receiving your email confirmation, you can request manual verification.

+
+
+ @endif + + @endif @endsection diff --git a/resources/views/admin/reports/home.blade.php b/resources/views/admin/reports/home.blade.php index 254772ce2..8f881567d 100644 --- a/resources/views/admin/reports/home.blade.php +++ b/resources/views/admin/reports/home.blade.php @@ -15,11 +15,14 @@ @endif - @php($ai = App\AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count()) - @php($spam = App\AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count()) - @if($ai || $spam) + + @if($ai || $spam || $mailVerifications)
+ +

{{$mailVerifications}}

+ Email Verify {{$mailVerifications == 1 ? 'Request' : 'Requests'}} +

{{$ai}}

Appeal {{$ai == 1 ? 'Request' : 'Requests'}} diff --git a/resources/views/admin/reports/mail_verification.blade.php b/resources/views/admin/reports/mail_verification.blade.php new file mode 100644 index 000000000..35ac8ccba --- /dev/null +++ b/resources/views/admin/reports/mail_verification.blade.php @@ -0,0 +1,75 @@ +@extends('admin.partial.template-full') + +@section('section') +
+
+
+
+ @foreach($reports as $report) +
+
+ +
+

{{ $report['username'] }}

+

{{ $report['email'] }}

+
+
+ + +
+
+
+ @endforeach + + @if(count($reports) == 0) +
+

No email verification requests found!

+
+ @endif +
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/settings/email.blade.php b/resources/views/settings/email.blade.php index b86bbe7ba..602eb9579 100644 --- a/resources/views/settings/email.blade.php +++ b/resources/views/settings/email.blade.php @@ -1,36 +1,63 @@ -@extends('settings.template') +@extends('layouts.app') -@section('section') +@section('content') +@if (session('status')) +
+ {{ session('status') }} +
+@endif +@if ($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }} + @endforeach +

+@endif +@if (session('error')) +
+ {{ session('error') }} +
+@endif -
-

Email Settings

+
+
+
+
+
+
+
+

Email Settings

+
+
+
+ @csrf + + + + +
+ + +

+ @if(Auth::user()->email_verified_at) + Verified {{Auth::user()->email_verified_at->diffForHumans()}} + @else + Unverified You need to verify your email. + @endif +

+
+
+
+ +
+
+
+
+
+
+
-
-
- @csrf - - - +
-
- -
- -

- @if(Auth::user()->email_verified_at) - Verified {{Auth::user()->email_verified_at->diffForHumans()}} - @else - Unverified You need to verify your email. - @endif -

-
-
-
-
-
- -
-
- -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/settings/password.blade.php b/resources/views/settings/password.blade.php index 6070a92c6..52782f102 100644 --- a/resources/views/settings/password.blade.php +++ b/resources/views/settings/password.blade.php @@ -1,38 +1,66 @@ -@extends('settings.template') +@extends('layouts.app') -@section('section') +@section('content') +@if (session('status')) +
+ {{ session('status') }} +
+@endif +@if ($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }} + @endforeach +

+@endif +@if (session('error')) +
+ {{ session('error') }} +
+@endif -
-

Update Password

+
+
+
+
+
+
+
+

Update Password

+
+
+
+ @csrf +
+ +
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+
+
-
-
- @csrf -
- -
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- -
-
-
+
-@endsection \ No newline at end of file +@endsection diff --git a/resources/views/settings/security/2fa/edit.blade.php b/resources/views/settings/security/2fa/edit.blade.php index 8b895da07..803e43b2a 100644 --- a/resources/views/settings/security/2fa/edit.blade.php +++ b/resources/views/settings/security/2fa/edit.blade.php @@ -1,33 +1,42 @@ -@extends('settings.template') +@extends('layouts.app') -@section('section') +@section('content') +
+
+
+
+
+
+
+

Edit Two-Factor Authentication

+
-
-

Edit Two-Factor Authentication

+
+ +

+ To register a new device, you have to remove any active devices. +

+ +
+
+ Authenticator App +
+
+ +

+ Added {{$user->{'2fa_setup_at'}->diffForHumans()}} +

+
+ +
+
+
+
- -
- -

- To register a new device, you have to remove any active devices. -

- -
-
- Authenticator App -
-
- -

- Added {{$user->{'2fa_setup_at'}->diffForHumans()}} -

-
- -
- +
@endsection @push('scripts') @@ -79,4 +88,4 @@ $(document).ready(function() { }); -@endpush \ No newline at end of file +@endpush diff --git a/resources/views/settings/security/2fa/recovery-codes.blade.php b/resources/views/settings/security/2fa/recovery-codes.blade.php index 9b6c61e4a..9add9cade 100644 --- a/resources/views/settings/security/2fa/recovery-codes.blade.php +++ b/resources/views/settings/security/2fa/recovery-codes.blade.php @@ -1,32 +1,42 @@ -@extends('settings.template') +@extends('layouts.app') -@section('section') +@section('content') +
+
+
+
+
+
+
+

Two-Factor Authentication Recovery Codes

+
-
-

Two-Factor Authentication Recovery Codes

-
- -
- @if(count($codes) > 0) -

- Each code can only be used once. -

-
    - @foreach($codes as $code) -
  • {{$code}}
  • - @endforeach -
- @else -
-

You are out of recovery codes

-

Generate more recovery codes and store them in a safe place.

-

-

- @csrf - -
-

+
+ @if(count($codes) > 0) +

+ Each code can only be used once. +

+
    + @foreach($codes as $code) +
  • {{$code}}
  • + @endforeach +
+ @else +
+

You are out of recovery codes

+

Generate more recovery codes and store them in a safe place.

+

+

+ @csrf + +
+

+
+ @endif +
+
+
- @endif - -@endsection \ No newline at end of file +
+
+@endsection diff --git a/resources/views/settings/security/2fa/setup.blade.php b/resources/views/settings/security/2fa/setup.blade.php index d8b3290b2..39e83a9c6 100644 --- a/resources/views/settings/security/2fa/setup.blade.php +++ b/resources/views/settings/security/2fa/setup.blade.php @@ -1,85 +1,97 @@ -@extends('settings.template') +@extends('layouts.app') -@section('section') +@section('content') +
+
+
+
+
+
+
+

Setup Two-Factor Authentication

+
+
+
+ We only support Two-Factor Authentication via TOTP mobile apps. +
+
+
+ Step 1: Install compatible 2FA mobile app +
+
+
+

You will need to install a compatible mobile app, we recommend the following apps:

+ +
+
-
-

Setup Two-Factor Authentication

+
+ +
+
+

Please scan the QR code and then enter the 6 digit code in the form below. Keep in mind the code changes every 30 seconds, and is only good for 1 minute.

+
+
+
+

QR Code

+ +
+
+

OTP Secret

+ +
+
+
+
+
+ + +
+ +
+
+
+
+
+ +
+
+ Step 3: Download Backup Codes +
+
+
+

Please store the following codes in a safe place, each backup code can be used only once if you do not have access to your 2FA mobile app.

+ + + @foreach($backups as $code) +

{{$code}}

+ @endforeach +
+
+
+ +
+
+
+
-
-
- We only support Two-Factor Authentication via TOTP mobile apps. -
-
-
- Step 1: Install compatible 2FA mobile app -
-
-
-

You will need to install a compatible mobile app, we recommend the following apps:

- -
-
- -
- -
-
-

Please scan the QR code and then enter the 6 digit code in the form below. Keep in mind the code changes every 30 seconds, and is only good for 1 minute.

-
-
-
-

QR Code

- -
-
-

OTP Secret

- -
-
-
-
-
- - -
- -
-
-
-
-
- -
-
- Step 3: Download Backup Codes -
-
-
-

Please store the following codes in a safe place, each backup code can be used only once if you do not have access to your 2FA mobile app.

- - - @foreach($backups as $code) -

{{$code}}

- @endforeach -
-
-
+
@endsection @push('scripts') @@ -138,4 +150,4 @@ $(document).ready(function() { }); }); -@endpush \ No newline at end of file +@endpush diff --git a/routes/web.php b/routes/web.php index 0a1b50850..d0ac72ecb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -14,6 +14,10 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('reports/appeals', 'AdminController@appeals'); Route::get('reports/appeal/{id}', 'AdminController@showAppeal'); Route::post('reports/appeal/{id}', 'AdminController@updateAppeal'); + Route::get('reports/email-verifications', 'AdminController@reportMailVerifications'); + Route::post('reports/email-verifications/ignore', 'AdminController@reportMailVerifyIgnore'); + Route::post('reports/email-verifications/approve', 'AdminController@reportMailVerifyApprove'); + Route::post('reports/email-verifications/clear-ignored', 'AdminController@reportMailVerifyClearIgnored'); Route::redirect('stories', '/stories/list'); Route::get('stories/list', 'AdminController@stories')->name('admin.stories'); Route::redirect('statuses', '/statuses/list'); @@ -273,6 +277,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('verify-email', 'AccountController@verifyEmail'); Route::post('verify-email', 'AccountController@sendVerifyEmail'); + Route::get('verify-email/request', 'InternalApiController@requestEmailVerification'); + Route::post('verify-email/request', 'InternalApiController@requestEmailVerificationStore'); Route::get('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail'); Route::get('auth/sudo', 'AccountController@sudoMode');