feat: add invite notification emails, functionality to admin dashboard and sign up page
Signed-off-by: Miguel Nogueira <me@nogueira.codes>
This commit is contained in:
147
app/Http/Controllers/InvitationController.php
Normal file
147
app/Http/Controllers/InvitationController.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\InvitationRequest;
|
||||
use App\Invitation;
|
||||
use App\Mail\InviteApprovedMail;
|
||||
use App\Mail\InvitedToApp;
|
||||
use App\Mail\InviteRequestReceived;
|
||||
use App\Response;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Mail;
|
||||
use Session;
|
||||
|
||||
class InvitationController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('dashboard.administration.invites', [
|
||||
'invites' => Invitation::all()
|
||||
]);
|
||||
}
|
||||
|
||||
public function requestInvite(InvitationRequest $request)
|
||||
{
|
||||
|
||||
$guest = Auth::guest();
|
||||
$invitation = new Invitation();
|
||||
|
||||
$invitation->requestor_email = $request->input('email');
|
||||
$invitation->requestor_ip_address = $request->ip();
|
||||
$invitation->status = $guest ? 'pending' : 'approved';
|
||||
$invitation->notified = !$guest; // confirmation msg doesn't count
|
||||
$invitation->invitation_code = bin2hex(random_bytes(64));
|
||||
$invitation->expiration = now()->addDays(2);
|
||||
|
||||
try {
|
||||
$invitation->saveOrFail();
|
||||
$addlMessage = ($guest) ? __('Check your email address for a confirmation email.') : '';
|
||||
|
||||
$request->session()->flash('success', __('Invitation request sent. :additionalUnauthenticatedMessage', ['additionalUnauthenticatedMessage' => $addlMessage]));
|
||||
|
||||
if ($guest) {
|
||||
Mail::to($invitation->requestor_email)->send(new InviteRequestReceived());
|
||||
}
|
||||
else {
|
||||
// this is an approved invite
|
||||
Mail::to($invitation->requestor_email)->send(new InvitedToApp($invitation));
|
||||
}
|
||||
|
||||
|
||||
} catch (\Exception $exception) {
|
||||
|
||||
\Log::debug('[INVITES]: Error saving invite request', ['message' => $exception->getMessage(), 'requestor_ip' => $request->ip()]);
|
||||
$request->session()->flash('error', __('Sorry, but we were unable to request an invitation for you. If you already requested one, trying to request another will not be possible, nor will it speed up the process.'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function approveInvite(Request $request, Invitation $invitation)
|
||||
{
|
||||
$approvableStates = [
|
||||
'pending'
|
||||
];
|
||||
|
||||
if ($invitation->expiration && now()->lessThanOrEqualTo($invitation->expiration) && in_array($invitation->status, $approvableStates))
|
||||
{
|
||||
$invitation->status = 'approved';
|
||||
$invitation->notified = true;
|
||||
$invitation->save();
|
||||
|
||||
Mail::to($invitation->requestor_email)->send(new InviteApprovedMail($invitation));
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', __('Invite request approved! This user can now sign up.'));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', __('This invitation couldn\'t be approved because either it\'s already approved or it is expired.'));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function denyInvite(Request $request, Invitation $invitation)
|
||||
{
|
||||
$declinableStates = [
|
||||
'pending'
|
||||
];
|
||||
|
||||
if ($invitation->expiration && now()->lessThanOrEqualTo($invitation->expiration) && in_array($invitation->status, $declinableStates))
|
||||
{
|
||||
$invitation->status = 'denied';
|
||||
$invitation->save();
|
||||
|
||||
return redirect()
|
||||
->with('success', __('Invitation denied. No notifications were sent. This user cannot be invited again.'))
|
||||
->back();
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->with('error', __('This invitation could not be denied because it is either already approved, expired, or in an otherwise invalid state.'));
|
||||
}
|
||||
|
||||
public function redeemInvite(Request $request)
|
||||
{
|
||||
return view('auth.redeem-invite', ['validationToken' => $request->route('token')]);
|
||||
}
|
||||
|
||||
public function validateInvite(Request $request)
|
||||
{
|
||||
$token = $request->input('validation_token');
|
||||
$email = $request->input('email');
|
||||
|
||||
$invite = Invitation::where('requestor_email', $email)->first();
|
||||
|
||||
|
||||
|
||||
if (!empty($invite) && $token === $invite->invitation_code && 'approved' === $invite->status && $invite->expiration && now()->lessThanOrEqualTo($invite->expiration))
|
||||
{
|
||||
$invite->status = 'completed';
|
||||
$invite->save();
|
||||
|
||||
Session::put('ALLOW_REGISTRATION_OVERRIDE', true);
|
||||
Session::put('REGISTRATION_OVERRIDE_EMAIL', $email);
|
||||
|
||||
return redirect()
|
||||
->route('register')
|
||||
->with('success', __('Invitation code validated! You can now sign up with the email address you were invited with.'));
|
||||
}
|
||||
else
|
||||
{
|
||||
return redirect()
|
||||
->back()
|
||||
->with('error', __('Something went wrong while validating your invite. Either it does not exist, is expired, has not been approved yet, or the token is wrong (do not edit it).'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
31
app/Http/Requests/InvitationRequest.php
Normal file
31
app/Http/Requests/InvitationRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Facades\Options;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InvitationRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'email', 'max:254'],
|
||||
];
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (Options::getOption('enable_registrations')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function failedAuthorization()
|
||||
{
|
||||
throw new AuthorizationException(__('You cannot request a new invite for this user/e-mail address right now. Keep in mind that users can only be invited once.'));
|
||||
}
|
||||
}
|
18
app/Invitation.php
Normal file
18
app/Invitation.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Policies\InvitationPolicy;
|
||||
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
#[UsePolicy(InvitationPolicy::class)]
|
||||
class Invitation extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'expiration' => 'datetime'
|
||||
];
|
||||
}
|
||||
}
|
43
app/Mail/InviteApprovedMail.php
Normal file
43
app/Mail/InviteApprovedMail.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Invitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InviteApprovedMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public Invitation $invitation
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your invite for ' . config('app.name') . 'has just been approved',
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'mail.invite-approved',
|
||||
with: [
|
||||
'token' => $this->invitation->invitation_code,
|
||||
'invitedDaysSince' => $this->invitation->created_at->diffInDays(now())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
53
app/Mail/InviteRequestReceived.php
Normal file
53
app/Mail/InviteRequestReceived.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InviteRequestReceived extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your invite request has been received',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'mail.invited-request-received',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
57
app/Mail/InvitedToApp.php
Normal file
57
app/Mail/InvitedToApp.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Invitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class InvitedToApp extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(public Invitation $invitation)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'You\'ve just been invited to ' . config('app.name'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'mail.invited-to-app',
|
||||
with: [
|
||||
'token' => $this->invitation->invitation_code
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
37
app/Policies/InvitationPolicy.php
Normal file
37
app/Policies/InvitationPolicy.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Invitation;
|
||||
use App\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class InvitationPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function view(User $user, Invitation $invitation): Response
|
||||
{
|
||||
return $user->can('admin.manageInvitations') ? Response::allow() : Response::deny(__('You do not have permission to view invitations.'));
|
||||
}
|
||||
|
||||
public function create(?User $user): Response
|
||||
{
|
||||
if (is_null($user)) {
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
return $user->can('admin.manageInvitations') ? Response::allow() : Response::deny(__('You do not have permission to request invitations.'));
|
||||
}
|
||||
|
||||
public function delete(User $user, Invitation $invitation): Response
|
||||
{
|
||||
return $user->can('admin.manageInvitations') ? Response::allow() : Response::deny(__('You do not have permission to revoke invitations.'));
|
||||
}
|
||||
}
|
@@ -26,11 +26,13 @@ use App\Application;
|
||||
use App\Appointment;
|
||||
use App\Ban;
|
||||
use App\Form;
|
||||
use App\Invitation;
|
||||
use App\Policies\AbsencePolicy;
|
||||
use App\Policies\ApplicationPolicy;
|
||||
use App\Policies\AppointmentPolicy;
|
||||
use App\Policies\BanPolicy;
|
||||
use App\Policies\FormPolicy;
|
||||
use App\Policies\InvitationPolicy;
|
||||
use App\Policies\ProfilePolicy;
|
||||
use App\Policies\TeamFilePolicy;
|
||||
use App\Policies\TeamPolicy;
|
||||
@@ -68,6 +70,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
Team::class => TeamPolicy::class,
|
||||
TeamFile::class => TeamFilePolicy::class,
|
||||
Absence::class => AbsencePolicy::class,
|
||||
Invitation::class => InvitationPolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -80,11 +83,11 @@ class AuthServiceProvider extends ServiceProvider
|
||||
|
||||
VerifyEmail::toMailUsing(function ($notifiable, $url) {
|
||||
return (new MailMessage)
|
||||
->greeting("Hi {$notifiable->name}! Welcome to ".config('app.name').'.')
|
||||
->greeting("Hi {$notifiable->name}! Welcome to " . config('app.name') . '.')
|
||||
->line('To finish setting up your account, you must verify your email. This is to ensure only real users access our website.')
|
||||
->line('If you didn\'t sign up for an account, you can safely ignore this email.')
|
||||
->action('Verify account', $url)
|
||||
->salutation('The team at '.config('app.name'));
|
||||
->salutation('The team at ' . config('app.name'));
|
||||
});
|
||||
|
||||
Gate::define('viewLogViewer', function (?User $user) {
|
||||
|
Reference in New Issue
Block a user