Add manual email verification requests

This commit is contained in:
Daniel Supernault 2021-11-08 23:02:34 -07:00
parent 9bd53524c7
commit bc65938757
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
7 changed files with 252 additions and 32 deletions

View file

@ -4,8 +4,12 @@ namespace App\Http\Controllers\Admin;
use Cache; use Cache;
use App\Report; use App\Report;
use App\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Services\AccountService;
use App\Services\StatusService;
trait AdminReportController trait AdminReportController
{ {
@ -33,6 +37,7 @@ trait AdminReportController
$report = Report::findOrFail($id); $report = Report::findOrFail($id);
$this->handleReportAction($report, $action); $this->handleReportAction($report, $action);
Cache::forget('admin-dash:reports:list-cache');
return response()->json(['msg'=> 'Success']); return response()->json(['msg'=> 'Success']);
} }
@ -52,17 +57,20 @@ trait AdminReportController
$item->is_nsfw = true; $item->is_nsfw = true;
$item->save(); $item->save();
$report->nsfw = true; $report->nsfw = true;
StatusService::del($item->id);
break; break;
case 'unlist': case 'unlist':
$item->visibility = 'unlisted'; $item->visibility = 'unlisted';
$item->save(); $item->save();
Cache::forget('profiles:private'); Cache::forget('profiles:private');
StatusService::del($item->id);
break; break;
case 'delete': case 'delete':
// Todo: fire delete job // Todo: fire delete job
$report->admin_seen = null; $report->admin_seen = null;
StatusService::del($item->id);
break; break;
case 'shadowban': case 'shadowban':
@ -115,4 +123,55 @@ trait AdminReportController
]; ];
return response()->json($res); 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];
}
} }

View file

@ -17,6 +17,7 @@ use App\{
use DB, Cache; use DB, Cache;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Http\Controllers\Admin\{ use App\Http\Controllers\Admin\{
AdminDiscoverController, AdminDiscoverController,
AdminInstanceController, AdminInstanceController,
@ -28,12 +29,13 @@ use App\Http\Controllers\Admin\{
}; };
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use App\Services\AdminStatsService; use App\Services\AdminStatsService;
use App\Services\StatusService;
use App\Services\StoryService; use App\Services\StoryService;
class AdminController extends Controller class AdminController extends Controller
{ {
use AdminReportController, use AdminReportController,
AdminDiscoverController, AdminDiscoverController,
AdminMediaController, AdminMediaController,
AdminSettingsController, AdminSettingsController,
AdminInstanceController, AdminInstanceController,
@ -54,9 +56,15 @@ class AdminController extends Controller
public function statuses(Request $request) public function statuses(Request $request)
{ {
$statuses = Status::orderBy('id', 'desc')->simplePaginate(10); $statuses = Status::orderBy('id', 'desc')->cursorPaginate(10);
$data = $statuses->map(function($status) {
return view('admin.statuses.home', compact('statuses')); 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) public function showStatus(Request $request, $id)
@ -69,17 +77,45 @@ class AdminController extends Controller
public function reports(Request $request) public function reports(Request $request)
{ {
$filter = $request->input('filter') == 'closed' ? 'closed' : 'open'; $filter = $request->input('filter') == 'closed' ? 'closed' : 'open';
$reports = Report::whereHas('status') $page = $request->input('page') ?? 1;
->whereHas('reportedUser')
->whereHas('reporter') $ai = Cache::remember('admin-dash:reports:ai-count', 3600, function() {
->orderBy('created_at','desc') return AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count();
->when($filter, function($q, $filter) { });
return $filter == 'open' ?
$q->whereNull('admin_seen') : $spam = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
$q->whereNotNull('admin_seen'); return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
}) });
->paginate(6);
return view('admin.reports.home', compact('reports')); $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) 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:exemption_by_pid:' . $appeal->user->profile_id);
Cache::forget('pf:bouncer_v0:recent_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'); return redirect('/i/admin/reports/autospam');
} }
@ -156,8 +192,11 @@ class AdminController extends Controller
$appeal->appeal_handled_at = now(); $appeal->appeal_handled_at = now();
$appeal->save(); $appeal->save();
StatusService::del($status->id);
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_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('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
Cache::forget('admin-dash:reports:spam-count');
return redirect('/i/admin/reports/autospam'); return redirect('/i/admin/reports/autospam');
} }
@ -176,7 +215,7 @@ class AdminController extends Controller
if($action == 'dismiss') { if($action == 'dismiss') {
$appeal->appeal_handled_at = now(); $appeal->appeal_handled_at = now();
$appeal->save(); $appeal->save();
Cache::forget('admin-dash:reports:ai-count');
return redirect('/i/admin/reports/appeals'); return redirect('/i/admin/reports/appeals');
} }
@ -201,6 +240,8 @@ class AdminController extends Controller
$appeal->appeal_handled_at = now(); $appeal->appeal_handled_at = now();
$appeal->save(); $appeal->save();
StatusService::del($status->id);
Cache::forget('admin-dash:reports:ai-count');
return redirect('/i/admin/reports/appeals'); return redirect('/i/admin/reports/appeals');
} }

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{ use App\{
AccountInterstitial, AccountInterstitial,
Bookmark,
DirectMessage, DirectMessage,
DiscoverCategory, DiscoverCategory,
Hashtag, Hashtag,
@ -19,6 +20,7 @@ use App\{
UserFilter, UserFilter,
}; };
use Auth,Cache; use Auth,Cache;
use Illuminate\Support\Facades\Redis;
use Carbon\Carbon; use Carbon\Carbon;
use League\Fractal; use League\Fractal;
use App\Transformer\Api\{ use App\Transformer\Api\{
@ -345,14 +347,18 @@ class InternalApiController extends Controller
public function bookmarks(Request $request) public function bookmarks(Request $request)
{ {
$statuses = Auth::user()->profile $res = Bookmark::whereProfileId($request->user()->profile_id)
->bookmarks() ->orderByDesc('created_at')
->withCount(['likes','comments']) ->simplePaginate(10)
->orderBy('created_at', 'desc') ->map(function($bookmark) {
->simplePaginate(10); $status = StatusService::get($bookmark->status_id);
$status['bookmarked_at'] = $bookmark->created_at->format('c');
$resource = new Fractal\Resource\Collection($statuses, new StatusTransformer()); return $status;
$res = $this->fractal->createData($resource)->toArray(); })
->filter(function($bookmark) {
return isset($bookmark['id']);
})
->values();
return response()->json($res); return response()->json($res);
} }
@ -456,4 +462,18 @@ class InternalApiController extends Controller
$template = $status->in_reply_to_id ? 'status.reply' : 'status.remote'; $template = $status->in_reply_to_id ? 'status.reply' : 'status.remote';
return view($template, compact('user', 'status')); 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!']);
}
} }

View file

@ -13,19 +13,35 @@
<p class="font-weight-bold mb-0">{{ session('error') }}</p> <p class="font-weight-bold mb-0">{{ session('error') }}</p>
</div> </div>
@endif @endif
@if(Auth::user()->email_verified_at)
<p class="lead text-center mt-5">Your email is already verified. <a href="/" class="font-weight-bold">Click here</a> to go home.</p>
@else
<div class="card shadow-none border"> <div class="card shadow-none border">
<div class="card-header font-weight-bold bg-white">Confirm Email Address</div> <div class="card-header font-weight-bold bg-white">Confirm Email Address</div>
<div class="card-body"> <div class="card-body">
<p class="lead">You need to confirm your email address (<span class="font-weight-bold">{{Auth::user()->email}}</span>) before you can proceed.</p> <p class="lead text-break">You need to confirm your email address <span class="font-weight-bold">{{Auth::user()->email}}</span> before you can proceed.</p>
<p class="lead">You can change your email address <a href="/settings/email">here</a>.</p> @if(!$recentSent)
<p class="small">If you don't recieve an email within 30 minutes, you can <a href="/site/contact">contact the administrator</a>.</p>
<hr>
<form method="post"> <form method="post">
@csrf @csrf
<button type="submit" class="btn btn-primary btn-block py-1 font-weight-bold">Send Confirmation Email</button> <button type="submit" class="btn btn-primary btn-block py-1 font-weight-bold">Send Confirmation Email</button>
</form> </form>
@else
<button class="btn btn-primary btn-block py-1 font-weight-bold" disabled>Confirmation Email Sent</button>
@endif
<p class="mt-3 mb-0 small text-muted"><a href="/settings/email" class="font-weight-bold">Click here</a> to change your email address.</p>
</div> </div>
</div> </div>
@if($recentSent)
<div class="card mt-3 border shadow-none">
<div class="card-body">
<p class="mb-0 text-muted">If you are experiencing issues receiving your email confirmation, you can <a href="/i/verify-email/request" class="font-weight-bold">request manual verification</a>.</p>
</div>
</div>
@endif
@endif
</div> </div>
</div> </div>
@endsection @endsection

View file

@ -15,11 +15,14 @@
@endif @endif
</div> </div>
</div> </div>
@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 || $mailVerifications)
@if($ai || $spam)
<div class="col-12 col-md-8 offset-md-2"> <div class="col-12 col-md-8 offset-md-2">
<div class="mb-4"> <div class="mb-4">
<a class="btn btn-outline-primary px-5 py-3 mr-3" href="/i/admin/reports/email-verifications">
<p class="font-weight-bold h4 mb-0">{{$mailVerifications}}</p>
Email Verify {{$mailVerifications == 1 ? 'Request' : 'Requests'}}
</a>
<a class="btn btn-outline-primary px-5 py-3 mr-3" href="/i/admin/reports/appeals"> <a class="btn btn-outline-primary px-5 py-3 mr-3" href="/i/admin/reports/appeals">
<p class="font-weight-bold h4 mb-0">{{$ai}}</p> <p class="font-weight-bold h4 mb-0">{{$ai}}</p>
Appeal {{$ai == 1 ? 'Request' : 'Requests'}} Appeal {{$ai == 1 ? 'Request' : 'Requests'}}

View file

@ -0,0 +1,75 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title mb-3 d-flex justify-content-between align-items-center">
<div>
<h3 class="font-weight-bold d-inline-block">Email Verification Requests</h3>
@if($ignored)
<p>
You are ignoring <strong>{{ count($ignored) }}</strong> mail verification requests. <a href="#" class="font-weight-bold clear-ignored">Clear ignored requests</a>
</p>
@endif
</div>
<div class="float-right">
</div>
</div>
<div class="col-12 col-md-8 offset-md-2">
<div class="card shadow-none border">
<div class="list-group list-group-flush">
@foreach($reports as $report)
<div class="list-group-item">
<div class="media align-items-center">
<img src="{{ $report['avatar'] }}" width="50" height="50" class="rounded-circle border mr-3">
<div class="media-body">
<p class="font-weight-bold mb-0">{{ $report['username'] }}</p>
<p class="text-muted mb-0">{{ $report['email'] }}</p>
</div>
<div>
<button class="action-btn btn btn-light font-weight-bold mr-2" data-action="ignore" data-id="{{$report['id']}}">Ignore</button>
<button class="action-btn btn btn-primary font-weight-bold" data-action="approve" data-id="{{$report['id']}}"><i class="far fa-check-circle fa-lg mr-2"></i>Approve</button>
</div>
</div>
</div>
@endforeach
@if(count($reports) == 0)
<div class="list-group-item">
<p class="font-weight-bold mb-0">No email verification requests found!</p>
</div>
@endif
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
$('.clear-ignored').click((e) => {
e.preventDefault();
if(!window.confirm('Are you sure you want to clear all ignored requests?')) {
return;
}
axios.post('/i/admin/reports/email-verifications/clear-ignored')
.then(res => {
location.reload();
});
});
$('.action-btn').click((e) => {
e.preventDefault();
let type = e.currentTarget.getAttribute('data-action');
let id = e.currentTarget.getAttribute('data-id');
if(!window.confirm(`Are you sure you want to ${type} this email verification request?`)) {
return;
}
axios.post('/i/admin/reports/email-verifications/' + type, {
id: id
}).then(res => {
location.href = '/i/admin/reports';
}).catch(err => {
swal('Oops!', 'An error occured', 'error');
console.log(err);
})
});
</script>
@endpush

View file

@ -14,6 +14,10 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::get('reports/appeals', 'AdminController@appeals'); Route::get('reports/appeals', 'AdminController@appeals');
Route::get('reports/appeal/{id}', 'AdminController@showAppeal'); Route::get('reports/appeal/{id}', 'AdminController@showAppeal');
Route::post('reports/appeal/{id}', 'AdminController@updateAppeal'); 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::redirect('stories', '/stories/list');
Route::get('stories/list', 'AdminController@stories')->name('admin.stories'); Route::get('stories/list', 'AdminController@stories')->name('admin.stories');
Route::redirect('statuses', '/statuses/list'); 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::get('verify-email', 'AccountController@verifyEmail');
Route::post('verify-email', 'AccountController@sendVerifyEmail'); 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('confirm-email/{userToken}/{randomToken}', 'AccountController@confirmVerifyEmail');
Route::get('auth/sudo', 'AccountController@sudoMode'); Route::get('auth/sudo', 'AccountController@sudoMode');