Implemented e-mail confirmations
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
|
||||
use App\Models\EmailConfirmation;
|
||||
use App\Models\User;
|
||||
use Hash;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -22,34 +23,29 @@ class AccountController extends Controller
|
||||
|
||||
public function storeAccount(Request $request)
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = user();
|
||||
|
||||
$input = $this->validate($request, [
|
||||
'name' => [
|
||||
'required',
|
||||
VALI_NAME,
|
||||
Rule::unique('users')->ignoreModel(\user()),
|
||||
Rule::unique('users')->ignoreModel($user),
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
VALI_EMAIL,
|
||||
Rule::unique('users')->ignoreModel(\user()),
|
||||
Rule::unique('users')->ignoreModel($user),
|
||||
],
|
||||
'new_password' => ['nullable', 'confirmed', VALI_PASSWORD],
|
||||
]);
|
||||
|
||||
$user = user();
|
||||
|
||||
if ($input->email != $user->email) {
|
||||
$confirmation = EmailConfirmation::create([
|
||||
'user_id' => $user->id,
|
||||
'email' => $input->email,
|
||||
'token' => Str::random(60),
|
||||
]);
|
||||
$user->sendEmailConfirmationLink($input->email);
|
||||
|
||||
flash()->warning("New e-mail confirmation sent to $input->email.")->important();
|
||||
flash()->warning("E-mail confirmation sent to $input->email.")->important();
|
||||
|
||||
// TODO send the e-mail
|
||||
|
||||
unset($input->email);
|
||||
unset($input->email); // prevent updating the field in the model via fill
|
||||
}
|
||||
|
||||
$user->fill($input->all());
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailConfirmation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ConfirmEmailController extends Controller
|
||||
{
|
||||
public function resendConfirmation()
|
||||
{
|
||||
$user = user();
|
||||
/** @var EmailConfirmation[]|Collection $ec */
|
||||
$ec = $user->emailConfirmations()->valid()->get();
|
||||
$email = '';
|
||||
|
||||
if ($ec->count() == 0) {
|
||||
user()->sendEmailConfirmationLink($email = $user->email);
|
||||
} else {
|
||||
user()->sendEmailConfirmationLink($email = $ec[0]->email);
|
||||
}
|
||||
|
||||
flash()->success("A new confirmation link was sent to your e-mail $email"); // not important, will fade
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function confirmEmailAndLogin(Request $request)
|
||||
{
|
||||
$input = $this->validate($request, [
|
||||
'token' => 'string|required',
|
||||
]);
|
||||
|
||||
$ec = EmailConfirmation::where('token', $input->token)->valid()->first();
|
||||
if (!$ec) abort(400, "Invalid or expired token.");
|
||||
|
||||
$u = $ec->user;
|
||||
if (!$u) abort(400, "User account does not exist.");
|
||||
|
||||
if ($ec->email) $u->email = $ec->email;
|
||||
$wasConfirmed = $u->confirmed;
|
||||
$u->confirmed = true;
|
||||
$u->save();
|
||||
$ec->delete();
|
||||
|
||||
\Auth::login($u, true);
|
||||
|
||||
if ($wasConfirmed) {
|
||||
// user just changed an e-mail
|
||||
flash()->success("Your new e-mail $ec->email was confirmed!")->important();
|
||||
} else {
|
||||
flash()->success("Your e-mail $ec->email was confirmed, your account is now active!")->important();
|
||||
}
|
||||
|
||||
return redirect(route('profile.view', $u->name));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailConfirmation;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -36,7 +37,7 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
$this->middleware('guest')->except(['logout', 'confirmEmailAndLogin']);
|
||||
}
|
||||
|
||||
public function username()
|
||||
|
||||
@@ -52,6 +52,7 @@ class Kernel extends HttpKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'activated' => \App\Http\Middleware\ActivatedAccount::class,
|
||||
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
class ActivatedAccount
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (guest() || !user()->confirmed) {
|
||||
abort(403, "Only users with active accounts can do this.");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* E-mail confirmation
|
||||
@@ -11,15 +13,29 @@ namespace App\Models;
|
||||
* @property string $token
|
||||
* @property string $email
|
||||
* @property-read User $user
|
||||
* @method $this|Builder valid()
|
||||
*/
|
||||
class EmailConfirmation extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
const UPDATED_AT = null; // disable update timestamp
|
||||
const EXPIRATION_H = 2; // 2h
|
||||
|
||||
/** Authoring user */
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function scopeValid(Builder $query)
|
||||
{
|
||||
return $query->where('created_at', '>=',
|
||||
Carbon::now()->subHours(self::EXPIRATION_H));
|
||||
}
|
||||
|
||||
public function scopeExpired(Builder $query)
|
||||
{
|
||||
return $query->where('created_at', '<',
|
||||
Carbon::now()->subHours(self::EXPIRATION_H));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Models;
|
||||
use AdamWathan\EloquentOAuth\OAuthIdentity;
|
||||
use App\Models\Concerns\InstanceCache;
|
||||
use App\Models\Concerns\Reportable;
|
||||
use App\Notifications\ConfirmEmail;
|
||||
use Hash;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
@@ -16,6 +18,7 @@ use Illuminate\Foundation\Auth\Access\Authorizable;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use MightyPork\Utils\Str;
|
||||
|
||||
/**
|
||||
* A user in the application
|
||||
@@ -36,6 +39,7 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
* @property-read TableComment[]|Collection $tableComments
|
||||
* @property-read Table[]|Collection $favouriteTables
|
||||
* @property-read Table[]|Collection $followedDiscussions
|
||||
* @property-read EmailConfirmation[]|Collection $emailConfirmations
|
||||
* @property-read ContentReport[]|Collection $authoredReports
|
||||
* @property-read Notification[]|Collection $notifications
|
||||
* @property-read Notification[]|Collection $readNotifications
|
||||
@@ -132,6 +136,12 @@ class User extends BaseModel implements
|
||||
return $this->hasMany(ContentReport::class, 'author_id');
|
||||
}
|
||||
|
||||
/** User's e-mail confirmations */
|
||||
public function emailConfirmations()
|
||||
{
|
||||
return $this->hasMany(EmailConfirmation::class);
|
||||
}
|
||||
|
||||
// --------- Relation Helpers ---------
|
||||
|
||||
public function addFavourite(Table $table)
|
||||
@@ -198,4 +208,18 @@ class User extends BaseModel implements
|
||||
->where('user_id', $this->id)
|
||||
->where('table_id', $table->id)->exists();
|
||||
}
|
||||
|
||||
public function sendEmailConfirmationLink($newEmail)
|
||||
{
|
||||
// delete old confirmation tokens
|
||||
$this->emailConfirmations()->delete();
|
||||
|
||||
$confirmation = EmailConfirmation::create([
|
||||
'user_id' => $this->id,
|
||||
'email' => $newEmail,
|
||||
'token' => Hash::make(Str::random(40)),
|
||||
]);
|
||||
|
||||
$this->notify(new ConfirmEmail($newEmail, $confirmation->token));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Lang;
|
||||
|
||||
class ConfirmEmail extends Notification
|
||||
{
|
||||
/**
|
||||
* The email confirm token.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* The new e-mail
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $newEmail;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($newEmail, $token)
|
||||
{
|
||||
$this->newEmail = $newEmail;
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Illuminate\Notifications\Messages\MailMessage
|
||||
*/
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
/** @var User $notifiable */
|
||||
$notifiable->email = $this->newEmail; // hack to send to the new e-mail
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('E-Mail Confirmation')
|
||||
|
||||
->line(
|
||||
'Please confirm that you just requested using this e-mail with the datatable.directory
|
||||
account '.$notifiable->handle.'. It will be used to login, to recover forgotten passwords,
|
||||
and for sending notifications. You can always opt out of e-mail notifications in the settings.')
|
||||
|
||||
->action('Confirm E-Mail', url(config('app.url').route('confirm-email', ['token' => $this->token], false)))
|
||||
|
||||
->line(
|
||||
'If you did not request this action, no further action is required. The confirmation link
|
||||
will expire in a few hours.');
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ function guest() {
|
||||
return \Auth::guest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \App\Models\User|null
|
||||
*/
|
||||
function user() {
|
||||
return \Auth::user();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,36 @@
|
||||
|
||||
<main class="py-4">
|
||||
<div class="container">
|
||||
{{-- Warning about not activated account --}}
|
||||
@auth
|
||||
@if(!user()->confirmed)
|
||||
<div class="alert alert-warning alert-important" role="alert" >
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" >×</button>
|
||||
Your account is not activated yet, some options are locked!<br/>
|
||||
@if(user()->emailConfirmations()->valid()->exists())
|
||||
{{--
|
||||
There is a pending confirmation, whose e-mail may differ from what
|
||||
the user has saved in the rpofile (due to email change attempt)
|
||||
--}}
|
||||
Check your e-mail (<u>{{user()->emailConfirmations()->valid()->first()->email}}</u>) for the confirmation link, or <a href="{{route('resend-email-confirmation')}}">request a new one</a>.
|
||||
The confirmation message could fall into your Spam folder.<br>
|
||||
If this address is not correct, you can fix it in your <a href="{{route('account.edit')}}">account settings</a>.
|
||||
@else
|
||||
{{-- There are no pending confirmations --}}
|
||||
@if(user()->email)
|
||||
{{-- User has e-mail set in the profile, probably from registration --}}
|
||||
You can confirm your e-mail (<u>{{$user->email}}</u>) by <a href="{{route('resend-email-confirmation')}}">requesting a confirmation link</a>.
|
||||
If this address is not correct, you can fix it in your <a href="{{route('account.edit')}}">account settings</a>.
|
||||
@else
|
||||
{{-- User has no e-mail, maybe registered through a provider without e-mails --}}
|
||||
Set your e-mail in <a href="{{route('account.edit')}}">account settings</a> and you
|
||||
will receive a confirmation link.
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
@include('flash::message')
|
||||
@yield('content')
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
@sr(Table actions)
|
||||
|
||||
{{-- Disabled until implemented --}}
|
||||
@if(false)
|
||||
@if(true)
|
||||
|
||||
@if(guest() || user()->ownsTable($table))
|
||||
@if(guest() || !user()->confirmed || user()->ownsTable($table))
|
||||
|
||||
{{-- Passive fork buttons with counter --}}
|
||||
<a href="" class="btn btn-outline-primary py-1 btn-sm" title="Forks"
|
||||
|
||||
@@ -8,6 +8,12 @@ use SocialNorm\ProviderUser;
|
||||
|
||||
Auth::routes();
|
||||
|
||||
Route::get('/auth/confirm-email', 'Auth\ConfirmEmailController@confirmEmailAndLogin')
|
||||
->name('confirm-email')->middleware('auth');
|
||||
|
||||
Route::get('/auth/resend-email-confirmation', 'Auth\ConfirmEmailController@resendConfirmation')
|
||||
->name('resend-email-confirmation');
|
||||
|
||||
// ----------------- SOCIAL LOGIN --------------------
|
||||
|
||||
function _loginVia($method) {
|
||||
|
||||
+17
-11
@@ -25,12 +25,19 @@ Route::get('/', 'DashController@view')->name('dash');
|
||||
|
||||
// redirect home to user home
|
||||
Route::get('/home', function () {
|
||||
if (guest()) return redirect('/');
|
||||
if (guest()) return redirect(route('dash'));
|
||||
|
||||
return redirect(route('profile.view', user()->name));
|
||||
})->name('home');
|
||||
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
// Table resource
|
||||
Route::get('@{user}', 'ProfileController@view')->name('profile.view');
|
||||
Route::get('@{user}/{table}', 'TableController@view')->name('table.view');
|
||||
Route::get('@{user}/{table}/export', 'TableController@export')->name('table.export');
|
||||
|
||||
|
||||
// Routes for confirmed users
|
||||
Route::group(['middleware' => ['auth', 'activated']], function () {
|
||||
// Table resource
|
||||
Route::group([
|
||||
'prefix' => 'table',
|
||||
@@ -39,6 +46,14 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::post('create', 'TableController@storeNew')->name('table.storeNew');
|
||||
});
|
||||
|
||||
Route::get('@{user}/{table}/settings', 'TableController@settings')->name('table.conf');
|
||||
Route::post('@{user}/{table}/settings', 'TableController@storeSettings')->name('table.storeConf');
|
||||
Route::post('@{user}/{table}/delete', 'TableController@delete')->name('table.delete');
|
||||
});
|
||||
|
||||
// Routes for all authed users
|
||||
Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
Route::group([
|
||||
'prefix' => 'profile',
|
||||
], function () {
|
||||
@@ -53,13 +68,4 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::post('store', 'AccountController@storeAccount')->name('account.store');
|
||||
Route::get('forget-social-login/{id}', 'AccountController@forgetSocialLogin')->name('forget-identity');
|
||||
});
|
||||
|
||||
Route::get('@{user}/{table}/settings', 'TableController@settings')->name('table.conf');
|
||||
Route::post('@{user}/{table}/settings', 'TableController@storeSettings')->name('table.storeConf');
|
||||
Route::post('@{user}/{table}/delete', 'TableController@delete')->name('table.delete');
|
||||
});
|
||||
|
||||
// Table resource - located at the end to work as a fallback
|
||||
Route::get('@{user}', 'ProfileController@view')->name('profile.view');
|
||||
Route::get('@{user}/{table}', 'TableController@view')->name('table.view');
|
||||
Route::get('@{user}/{table}/export', 'TableController@export')->name('table.export');
|
||||
|
||||
Reference in New Issue
Block a user