Prechádzať zdrojové kódy

Merge pull request #3002 from pixelfed/staging

Staging
daniel 3 rokov pred
rodič
commit
d4d92187db

+ 3 - 0
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))

+ 4 - 1
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)

+ 59 - 0
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];
+    }
 }

+ 58 - 17
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');
 	}

+ 28 - 8
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!']);
+	}
 }

+ 1 - 1
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/*',

+ 37 - 0
resources/views/account/email/request_verification.blade.php

@@ -0,0 +1,37 @@
+@extends('layouts.app')
+
+@section('content')
+<div class="container mt-4">
+  <div class="col-12 col-md-8 offset-md-2">
+	@if (session('status'))
+		<div class="alert alert-success">
+			<p class="font-weight-bold mb-0">{{ session('status') }}</p>
+		</div>
+	@endif
+	@if (session('error'))
+		<div class="alert alert-danger">
+			<p class="font-weight-bold mb-0">{{ session('error') }}</p>
+		</div>
+	@endif
+	<div class="card shadow-none border">
+	  <div class="card-header font-weight-bold bg-white">Request Manual Email Verification</div>
+	  <div class="card-body">
+	  	<p class="">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.</p>
+
+	  	<p class="font-weight-bold">If you request manual email verification, you still may experience issues recieving emails from our service, including password reset requests.</p>
+
+	  	<p>In the event you need to reset your password and do not receive the password reset email, please contact the administrators.</p>
+
+	  	@if(!$exists)
+	  	<form method="post">
+          @csrf
+          <button type="submit" class="btn btn-primary btn-block py-1 font-weight-bold">I understand, proceed with request</button>
+        </form>
+        @else
+        <button class="btn btn-primary btn-block py-1 font-weight-bold" disabled>Verification Request Sent</button>
+        @endif
+	  </div>
+  </div>
+</div>
+</div>
+@endsection

+ 20 - 4
resources/views/account/verify_email.blade.php

@@ -13,19 +13,35 @@
             <p class="font-weight-bold mb-0">{{ session('error') }}</p>
         </div>
     @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-header font-weight-bold bg-white">Confirm Email Address</div>
       <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">You can change your email address <a href="/settings/email">here</a>.</p>
-    	<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>
+        <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>
+        @if(!$recentSent)
         <form method="post">
           @csrf
           <button type="submit" class="btn btn-primary btn-block py-1 font-weight-bold">Send Confirmation Email</button>
         </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>
+
+    @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>
 @endsection

+ 6 - 3
resources/views/admin/reports/home.blade.php

@@ -15,11 +15,14 @@
 			@endif
 		</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)
+
+	@if($ai || $spam || $mailVerifications)
 	<div class="col-12 col-md-8 offset-md-2">
 		<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">
 				<p class="font-weight-bold h4 mb-0">{{$ai}}</p>
 				Appeal {{$ai == 1 ? 'Request' : 'Requests'}}

+ 75 - 0
resources/views/admin/reports/mail_verification.blade.php

@@ -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

+ 57 - 30
resources/views/settings/email.blade.php

@@ -1,36 +1,63 @@
-@extends('settings.template')
+@extends('layouts.app')
 
-@section('section')
+@section('content')
+@if (session('status'))
+    <div class="alert alert-primary px-3 h6 text-center">
+        {{ session('status') }}
+    </div>
+@endif
+@if ($errors->any())
+    <div class="alert alert-danger px-3 h6 text-center">
+            @foreach($errors->all() as $error)
+                <p class="font-weight-bold mb-1">{{ $error }}</li>
+            @endforeach
+    </div>
+@endif
+@if (session('error'))
+    <div class="alert alert-danger px-3 h6 text-center">
+        {{ session('error') }}
+    </div>
+@endif
 
-  <div class="title">
-    <h3 class="font-weight-bold">Email Settings</h3>
-  </div>
-  <hr>
-  <form method="post" action="{{route('settings.email')}}">
-    @csrf
-    <input type="hidden" class="form-control" name="name" value="{{Auth::user()->profile->name}}">
-    <input type="hidden" class="form-control" name="username" value="{{Auth::user()->profile->username}}">
-    <input type="hidden" class="form-control" name="website" value="{{Auth::user()->profile->website}}">
+<div class="container">
+  <div class="col-12">
+    <div class="card shadow-none border mt-5">
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 p-3 p-md-5">
+			  <div class="title">
+			    <h3 class="font-weight-bold">Email Settings</h3>
+			  </div>
+			  <hr>
+			  <form method="post" action="{{route('settings.email')}}">
+			    @csrf
+			    <input type="hidden" class="form-control" name="name" value="{{Auth::user()->profile->name}}">
+			    <input type="hidden" class="form-control" name="username" value="{{Auth::user()->profile->username}}">
+			    <input type="hidden" class="form-control" name="website" value="{{Auth::user()->profile->website}}">
 
-    <div class="form-group row">
-      <label for="email" class="col-sm-3 col-form-label font-weight-bold text-right">Email</label>
-      <div class="col-sm-9">
-        <input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}">
-        <p class="help-text small text-muted font-weight-bold">
-          @if(Auth::user()->email_verified_at)
-          <span class="text-success">Verified</span> {{Auth::user()->email_verified_at->diffForHumans()}}
-          @else
-          <span class="text-danger">Unverified</span> You need to <a href="/i/verify-email">verify your email</a>.
-          @endif
-        </p>
+			    <div class="form-group">
+			      <label for="email" class="font-weight-bold">Email Address</label>
+			        <input type="email" class="form-control" id="email" name="email" placeholder="Email Address" value="{{Auth::user()->email}}">
+			        <p class="help-text small text-muted font-weight-bold">
+			          @if(Auth::user()->email_verified_at)
+			          <span class="text-success">Verified</span> {{Auth::user()->email_verified_at->diffForHumans()}}
+			          @else
+			          <span class="text-danger">Unverified</span> You need to <a href="/i/verify-email">verify your email</a>.
+			          @endif
+			        </p>
+			    </div>
+			    <div class="form-group row">
+			      <div class="col-12 text-right">
+			        <button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
+			      </div>
+			    </div>
+			  </form>
+          </div>
+        </div>
       </div>
     </div>
-    <hr>
-    <div class="form-group row">
-      <div class="col-12 text-right">
-        <button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
-      </div>
-    </div>
-  </form>
+  </div>
+</div>
+
 
-@endsection
+@endsection

+ 59 - 31
resources/views/settings/password.blade.php

@@ -1,38 +1,66 @@
-@extends('settings.template')
+@extends('layouts.app')
 
-@section('section')
-
-  <div class="title">
-    <h3 class="font-weight-bold">Update Password</h3>
-  </div>
-  <hr>
-  <form method="post">
-    @csrf
-    <div class="form-group row">
-      <label for="existing" class="col-sm-3 col-form-label font-weight-bold">Current</label>
-      <div class="col-sm-9">
-        <input type="password" class="form-control" name="current" placeholder="Your current password">
-      </div>
+@section('content')
+@if (session('status'))
+    <div class="alert alert-primary px-3 h6 text-center">
+        {{ session('status') }}
     </div>
-    <hr>
-    <div class="form-group row">
-      <label for="new" class="col-sm-3 col-form-label font-weight-bold">New</label>
-      <div class="col-sm-9">
-        <input type="password" class="form-control" name="password" placeholder="Enter new password here">
-      </div>
+@endif
+@if ($errors->any())
+    <div class="alert alert-danger px-3 h6 text-center">
+            @foreach($errors->all() as $error)
+                <p class="font-weight-bold mb-1">{{ $error }}</li>
+            @endforeach
     </div>
-    <div class="form-group row">
-      <label for="confirm" class="col-sm-3 col-form-label font-weight-bold">Confirm</label>
-      <div class="col-sm-9">
-        <input type="password" class="form-control" name="password_confirmation" placeholder="Confirm new password">
-      </div>
+@endif
+@if (session('error'))
+    <div class="alert alert-danger px-3 h6 text-center">
+        {{ session('error') }}
     </div>
-    <hr>
-    <div class="form-group row">
-      <div class="col-12 text-right">
-        <button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
+@endif
+
+<div class="container">
+  <div class="col-12">
+    <div class="card shadow-none border mt-5">
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 p-3 p-md-5">
+			  <div class="title">
+			    <h3 class="font-weight-bold">Update Password</h3>
+			  </div>
+			  <hr>
+			  <form method="post">
+			    @csrf
+			    <div class="form-group row">
+			      <label for="existing" class="col-sm-3 col-form-label font-weight-bold">Current</label>
+			      <div class="col-sm-9">
+			        <input type="password" class="form-control" name="current" placeholder="Your current password">
+			      </div>
+			    </div>
+			    <hr>
+			    <div class="form-group row">
+			      <label for="new" class="col-sm-3 col-form-label font-weight-bold">New</label>
+			      <div class="col-sm-9">
+			        <input type="password" class="form-control" name="password" placeholder="Enter new password here">
+			      </div>
+			    </div>
+			    <div class="form-group row">
+			      <label for="confirm" class="col-sm-3 col-form-label font-weight-bold">Confirm</label>
+			      <div class="col-sm-9">
+			        <input type="password" class="form-control" name="password_confirmation" placeholder="Confirm new password">
+			      </div>
+			    </div>
+			    <div class="form-group row">
+			      <div class="col-12 text-right">
+			        <button type="submit" class="btn btn-primary font-weight-bold py-0 px-5">Submit</button>
+			      </div>
+			    </div>
+			  </form>
+			</div>
+        </div>
       </div>
     </div>
-  </form>
+  </div>
+</div>
 
-@endsection
+@endsection

+ 35 - 26
resources/views/settings/security/2fa/edit.blade.php

@@ -1,33 +1,42 @@
-@extends('settings.template')
+@extends('layouts.app')
 
-@section('section')
+@section('content')
+<div class="container">
+  <div class="col-12">
+    <div class="card shadow-none border mt-5">
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 p-3 p-md-5">
+			  <div class="title">
+			    <h3 class="font-weight-bold">Edit Two-Factor Authentication</h3>
+			  </div>
 
-  <div class="title">
-    <h3 class="font-weight-bold">Edit Two-Factor Authentication</h3>
-  </div>
-  
-  <hr>
+			  <hr>
 
-  <p class="lead pb-3">
-  	To register a new device, you have to remove any active devices.
-  </p>
+			  <p class="lead pb-3">
+			  	To register a new device, you have to remove any active devices.
+			  </p>
 
-  <div class="card">
-  	<div class="card-header bg-light font-weight-bold">
-  		Authenticator App
-  	</div>
-  	<div class="card-body d-flex justify-content-between align-items-center">
-  		<i class="fas fa-lock fa-3x text-success"></i>
-  		<p class="font-weight-bold mb-0">
-  			Added {{$user->{'2fa_setup_at'}->diffForHumans()}}
-  		</p>
-  	</div>
-  	<div class="card-footer bg-white text-right">
-  		<a class="btn btn-outline-secondary btn-sm px-4 font-weight-bold mr-3" href="{{route('settings.security.2fa.recovery')}}">View Recovery Codes</a>
-  		<a class="btn btn-outline-danger btn-sm px-4 font-weight-bold remove-device" href="#">Remove</a>
-  	</div>
+			  <div class="card">
+			  	<div class="card-header bg-light font-weight-bold">
+			  		Authenticator App
+			  	</div>
+			  	<div class="card-body d-flex justify-content-between align-items-center">
+			  		<i class="fas fa-lock fa-3x text-success"></i>
+			  		<p class="font-weight-bold mb-0">
+			  			Added {{$user->{'2fa_setup_at'}->diffForHumans()}}
+			  		</p>
+			  	</div>
+			  	<div class="card-footer bg-white text-right">
+			  		<a class="btn btn-outline-secondary btn-sm px-4 font-weight-bold mr-3" href="{{route('settings.security.2fa.recovery')}}">View Recovery Codes</a>
+			  		<a class="btn btn-outline-danger btn-sm px-4 font-weight-bold remove-device" href="#">Remove</a>
+			  	</div>
+			  </div>
+        </div>
+      </div>
+    </div>
   </div>
-
+</div>
 @endsection
 
 @push('scripts')
@@ -79,4 +88,4 @@ $(document).ready(function() {
 });
 
 </script>
-@endpush
+@endpush

+ 39 - 29
resources/views/settings/security/2fa/recovery-codes.blade.php

@@ -1,32 +1,42 @@
-@extends('settings.template')
+@extends('layouts.app')
 
-@section('section')
+@section('content')
+<div class="container">
+  <div class="col-12">
+    <div class="card shadow-none border mt-5">
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 p-3 p-md-5">
+			  <div class="title">
+			    <h3 class="font-weight-bold">Two-Factor Authentication Recovery Codes</h3>
+			  </div>
 
-  <div class="title">
-    <h3 class="font-weight-bold">Two-Factor Authentication Recovery Codes</h3>
-  </div>
-
-  <hr>
-    @if(count($codes) > 0)
-      <p class="lead pb-3">
-      	Each code can only be used once.
-      </p>
-      <ul class="list-group">
-      	@foreach($codes as $code)
-      	<li class="list-group-item"><code>{{$code}}</code></li>
-      	@endforeach
-      </ul>
-    @else
-    <div class="pt-5">
-      <h4 class="font-weight-bold">You are out of recovery codes</h4>
-      <p class="lead">Generate more recovery codes and store them in a safe place.</p>
-      <p>
-        <form method="post">
-          @csrf
-          <button type="submit" class="btn btn-primary font-weight-bold">Generate Recovery Codes</button>
-        </form>
-      </p>
+			  <hr>
+			    @if(count($codes) > 0)
+			      <p class="lead pb-3">
+			      	Each code can only be used once.
+			      </p>
+			      <ul class="list-group">
+			      	@foreach($codes as $code)
+			      	<li class="list-group-item"><code>{{$code}}</code></li>
+			      	@endforeach
+			      </ul>
+			    @else
+			    <div class="pt-5">
+			      <h4 class="font-weight-bold">You are out of recovery codes</h4>
+			      <p class="lead">Generate more recovery codes and store them in a safe place.</p>
+			      <p>
+			        <form method="post">
+			          @csrf
+			          <button type="submit" class="btn btn-primary font-weight-bold">Generate Recovery Codes</button>
+			        </form>
+			      </p>
+			    </div>
+			    @endif
+            </div>
+        </div>
+      </div>
     </div>
-    @endif
-
-@endsection
+  </div>
+</div>
+@endsection

+ 90 - 78
resources/views/settings/security/2fa/setup.blade.php

@@ -1,85 +1,97 @@
-@extends('settings.template')
+@extends('layouts.app')
 
-@section('section')
+@section('content')
+<div class="container">
+  <div class="col-12">
+    <div class="card shadow-none border mt-5">
+      <div class="card-body">
+        <div class="row">
+          <div class="col-12 p-3 p-md-5">
+			  <div class="title">
+			    <h3 class="font-weight-bold">Setup Two-Factor Authentication</h3>
+			  </div>
+			  <hr>
+			  <div class="alert alert-info font-weight-light mb-3">
+			  	We only support Two-Factor Authentication via TOTP mobile apps.
+			  </div>
+			  <section class="step-one pb-5">
+			  	<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step1" aria-expanded="true" aria-controls="step1" data-step="1">
+			  		Step 1: Install compatible 2FA mobile app <i class="float-right fas fa-chevron-down"></i>
+			  	</div>
+			  	<hr>
+			  	<div class="collapse show" id="step1">
+				  	<p>You will need to install a compatible mobile app, we recommend the following apps:</p>
+				  	<ul>
+				  		<li><a href="https://1password.com/downloads/" rel="nooopener nofollow">1Password</a></li>
+				  		<li><a href="https://authy.com/download/" rel="nooopener nofollow">Authy</a></li>
+				  		<li><a href="https://lastpass.com/auth/" rel="nooopener nofollow">LastPass Authenticator</a></li>
+				  		<li>
+				  			Google Authenticator
+				  			<a class="small" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_CA" rel="nooopener nofollow">
+				  				(android)
+				  			</a>
+				  			<a class="small" href="https://itunes.apple.com/ca/app/google-authenticator/id388497605?mt=8" rel="nooopener nofollow">
+				  				(iOS)
+				  			</a>
+				  		</li>
+				  		<li><a href="https://www.microsoft.com/en-us/account/authenticator" rel="nooopener nofollow">Microsoft Authenticator</a></li>
+				  	</ul>
+			  	</div>
+			  </section>
 
-  <div class="title">
-    <h3 class="font-weight-bold">Setup Two-Factor Authentication</h3>
-  </div>
-  <hr>
-  <div class="alert alert-info font-weight-light mb-3">
-  	We only support Two-Factor Authentication via TOTP mobile apps.
-  </div>
-  <section class="step-one pb-5">
-  	<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step1" aria-expanded="true" aria-controls="step1" data-step="1">
-  		Step 1: Install compatible 2FA mobile app <i class="float-right fas fa-chevron-down"></i>
-  	</div>
-  	<hr>
-  	<div class="collapse show" id="step1">
-	  	<p>You will need to install a compatible mobile app, we recommend the following apps:</p>
-	  	<ul>
-	  		<li><a href="https://1password.com/downloads/" rel="nooopener nofollow">1Password</a></li>
-	  		<li><a href="https://authy.com/download/" rel="nooopener nofollow">Authy</a></li>
-	  		<li><a href="https://lastpass.com/auth/" rel="nooopener nofollow">LastPass Authenticator</a></li>
-	  		<li>
-	  			Google Authenticator 
-	  			<a class="small" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_CA" rel="nooopener nofollow">
-	  				(android)
-	  			</a>
-	  			<a class="small" href="https://itunes.apple.com/ca/app/google-authenticator/id388497605?mt=8" rel="nooopener nofollow">
-	  				(iOS)
-	  			</a>
-	  		</li>
-	  		<li><a href="https://www.microsoft.com/en-us/account/authenticator" rel="nooopener nofollow">Microsoft Authenticator</a></li>
-	  	</ul>
-  	</div>
-  </section>
+			  <section class="step-two pb-5">
+			  	<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step2" aria-expanded="false" aria-controls="step2" data-step="2">
+			  		Step 2: Scan QR Code and confirm <i class="float-right fas fa-chevron-down"></i>
+			  	</div>
+			  	<hr>
+			  	<div class="collapse" id="step2">
+				  	<p>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.</p>
+				  	<div class="card">
+				  		<div class="card-body text-center">
+				  			<div class="pb-3">
+				  				<p class="font-weight-bold">QR Code</p>
+				  				<img src="data:image/png;base64,{{$qrcode}}" class="img-fluid" width="200px">
+				  			</div>
+				  			<div>
+				  				<p class="font-weight-bold">OTP Secret</p>
+				  				<input type="text" class="form-control" value="{{ $user->{'2fa_secret'} }}" disabled>
+				  			</div>
+				  		</div>
+				  		<div class="card-body">
+				  			<form id="confirm-code">
+					  			<div class="form-group">
+					  				<label class="font-weight-bold small">Code</label>
+					  				<input type="text" name="code" id="verifyCode" class="form-control" placeholder="Code" autocomplete="off">
+					  			</div>
+					  			<button type="submit" class="btn btn-primary font-weight-bold">Submit</button>
+				  			</form>
+				  		</div>
+				  	</div>
+			  	</div>
+			  </section>
 
-  <section class="step-two pb-5">
-  	<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step2" aria-expanded="false" aria-controls="step2" data-step="2">
-  		Step 2: Scan QR Code and confirm <i class="float-right fas fa-chevron-down"></i>
-  	</div>
-  	<hr>
-  	<div class="collapse" id="step2">
-	  	<p>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.</p>
-	  	<div class="card">
-	  		<div class="card-body text-center">
-	  			<div class="pb-3">
-	  				<p class="font-weight-bold">QR Code</p>
-	  				<img src="data:image/png;base64,{{$qrcode}}" class="img-fluid" width="200px">
-	  			</div>
-	  			<div>
-	  				<p class="font-weight-bold">OTP Secret</p>
-	  				<input type="text" class="form-control" value="{{ $user->{'2fa_secret'} }}" disabled>
-	  			</div>
-	  		</div>
-	  		<div class="card-body">
-	  			<form id="confirm-code">
-		  			<div class="form-group">
-		  				<label class="font-weight-bold small">Code</label>
-		  				<input type="text" name="code" id="verifyCode" class="form-control" placeholder="Code" autocomplete="off">
-		  			</div>
-		  			<button type="submit" class="btn btn-primary font-weight-bold">Submit</button>
-	  			</form>
-	  		</div>
-	  	</div>
-  	</div>
-  </section>
+			  <section class="step-three pb-5">
+			  	<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step3" aria-expanded="true" aria-controls="step3" data-step="3">
+			  		Step 3: Download Backup Codes <i class="float-right fas fa-chevron-down"></i>
+			  	</div>
+			  	<hr>
+			  	<div class="collapse" id="step3">
+				  	<p>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.</p>
 
-  <section class="step-three pb-5">
-  	<div class="sub-title font-weight-bold h5" data-toggle="collapse" data-target="#step3" aria-expanded="true" aria-controls="step3" data-step="3">
-  		Step 3: Download Backup Codes <i class="float-right fas fa-chevron-down"></i>
-  	</div>
-  	<hr>
-  	<div class="collapse" id="step3">
-	  	<p>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.</p>
+				  	<code>
+				  	@foreach($backups as $code)
+				  	<p class="mb-0">{{$code}}</p>
+				  	@endforeach
+				  	</code>
+			  	</div>
+			  </section>
 
-	  	<code>
-	  	@foreach($backups as $code)
-	  	<p class="mb-0">{{$code}}</p>
-	  	@endforeach
-	  	</code>
-  	</div>
-  </section>
+            </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
 @endsection
 
 @push('scripts')
@@ -138,4 +150,4 @@ $(document).ready(function() {
 	});
 });
 </script>
-@endpush
+@endpush

+ 6 - 0
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');