Add user invitation facilities RSM-5

Adds user invitation to teams, and framework for assigning taems
Also adds user acc. deletion.
This commit is contained in:
2020-10-08 19:19:10 +01:00
parent 75f4404259
commit 596a469e15
26 changed files with 828 additions and 49 deletions

View File

@@ -8,9 +8,11 @@ use App\Http\Requests\SendInviteRequest;
use App\Mail\InviteToTeam;
use App\Team;
use App\User;
use App\Vacancy;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Mpociot\Teamwork\Exceptions\UserNotInTeamException;
use Mpociot\Teamwork\Facades\Teamwork;
use Mpociot\Teamwork\TeamInvite;
@@ -23,8 +25,10 @@ class TeamController extends Controller
*/
public function index()
{
$teams = Team::with('users.roles')->get();
return view('dashboard.teams.teams')
->with('teams', Team::all());
->with('teams', $teams);
}
/**
@@ -45,11 +49,13 @@ class TeamController extends Controller
*/
public function store(NewTeamRequest $request)
{
Team::create([
$team = Team::create([
'name' => $request->teamName,
'owner_id' => Auth::user()->id
]);
Auth::user()->teams()->attach($team->id);
$request->session()->flash('success', 'Team successfully created.');
return redirect()->back();
}
@@ -75,7 +81,8 @@ class TeamController extends Controller
{
return view('dashboard.teams.edit-team')
->with('team', $team)
->with('users', User::all());
->with('users', User::all())
->with('vacancies', Vacancy::all());
}
/**
@@ -186,4 +193,20 @@ class TeamController extends Controller
return redirect()->to(route('teams.index'));
}
public function switchTeam(Request $request, Team $team)
{
try
{
Auth::user()->switchTeam($team);
$request->session()->flash('success', 'Switched teams! Your team dashboard will now use this context.');
}
catch(UserNotInTeamException $ex)
{
$request->session()->flash('error', 'You can\'t switch to a team you don\'t belong to.');
}
return redirect()->back();
}
}

View File

@@ -23,11 +23,13 @@ use App\Notifications\EmailChanged;
use App\Notifications\ChangedPassword;
use Spatie\Permission\Models\Role;
use App\Traits\ReceivesAccountTokens;
use Google2FA;
class UserController extends Controller
{
use ReceivesAccountTokens;
public function showStaffMembers()
{
@@ -220,7 +222,7 @@ class UserController extends Controller
if ($request->confirmPrompt == 'DELETE ACCOUNT')
{
$user->delete();
$user->forceDelete();
$request->session()->flash('success','User deleted successfully. PII has been erased.');
}
else
@@ -232,6 +234,7 @@ class UserController extends Controller
return redirect()->route('registeredPlayerList');
}
public function update(UpdateUserRequest $request, User $user)
{
@@ -356,4 +359,6 @@ class UserController extends Controller
//TODO: Dispatch event
return redirect()->back();
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class UserDeleteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if (Auth::user()->has2FA())
{
return [
'currentPassword' => 'required|password:web',
'otp' => 'required|integer|max:6'
];
}
return [
'currentPassword' => 'required|password:web'
];
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\User;
class UserAccountDeleteConfirmation extends Mailable
{
use Queueable, SerializesModels;
public $deleteToken;
public $cancelToken;
public $originalIP;
public $name;
public $userID;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, array $tokens, string $originalIP)
{
$this->deleteToken = $tokens['delete'];
$this->cancelToken = $tokens['cancel'];
$this->originalIP = $originalIP;
$this->name = $user->name;
$this->userID = $user->id;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('mail.deleted-account');
}
}

View File

@@ -8,6 +8,12 @@ use Illuminate\Support\Facades\Log;
class UserObserver
{
public function __construct()
{
Log::debug('User observer has been initialised and ready for use!');
}
/**
* Handle the user "created" event.
*
@@ -39,20 +45,28 @@ class UserObserver
public function deleting(User $user)
{
$user->profile()->delete();
Log::debug('Referential integrity cleanup: Deleted profile!');
$applications = $user->applications;
if (!$applications->isEmpty())
if ($user->isForceDeleting())
{
Log::debug('RIC: Now trying to delete applications and responses...');
foreach($applications as $application)
$user->profile->delete();
Log::debug('Referential integrity cleanup: Deleted profile!');
$applications = $user->applications;
if (!$applications->isEmpty())
{
// code moved to Application observer, where it gets rid of attached elements individually
Log::debug('RIC: Deleting application ' . $application->id);
$application->delete();
Log::debug('RIC: Now trying to delete applications and responses...');
foreach($applications as $application)
{
// code moved to Application observer, where it gets rid of attached elements individually
Log::debug('RIC: Deleting application ' . $application->id);
$application->delete();
}
}
}
else
{
Log::debug('RIC: Not cleaning up soft deleted models!');
}
Log::debug('RIC: Cleanup done!');
@@ -66,7 +80,6 @@ class UserObserver
*/
public function deleted(User $user)
{
//
}
/**
@@ -88,6 +101,8 @@ class UserObserver
*/
public function forceDeleted(User $user)
{
//
Log::info('Model has been force deleted', [
'modelID' => $user->id
]);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Services;
use App\Vacancy;
use App\Application;
class VacancyApplicationService
{
/**
* Finds all applications associated with $model.
*
* @param Vacancy $model The model you want to search through.
* @return Illuminate\Support\Collection A collection of applications
*/
public function findApplications(Vacancy $model)
{
$applications = collect([]);
foreach(Application::all() as $application)
{
if ($application->response->vacancy->id == $model->id)
{
$applications->push($application);
}
}
return $applications;
}
}

View File

@@ -13,4 +13,10 @@ class Team extends TeamworkTeam
'description',
'openJoin'
];
public function vacancies()
{
return $this->belongsToMany('App\Vacancy', 'team_has_vacancy');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Hash;
trait HandlesAccountTokens
{
public function generateAccountTokens()
{
$deleteToken = bin2hex(openssl_random_pseudo_bytes(32));
$cancelToken = bin2hex(openssl_random_pseudo_bytes(32));
$tokens = [
'delete' => Hash::make($deleteToken),
'cancel' => Hash::make($cancelToken)
];
$this->account_tokens = json_encode($tokens);
$this->save();
return [
'delete' => $deleteToken,
'cancel' => $cancelToken
];
}
public function verifyAccountToken(string $token, string $type): bool
{
$tokens = json_decode($this->account_tokens);
if ($type == 'deleteToken')
{
return Hash::check($token, $tokens->delete);
}
elseif ($type == 'cancelToken')
{
return Hash::check($token, $tokens->cancel);
}
return false;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Http\Requests\UserDeleteRequest;
use App\Mail\UserAccountDeleteConfirmation;
use App\User;
trait ReceivesAccountTokens
{
public function userDelete(UserDeleteRequest $request)
{
// a little verbose
$user = User::find(Auth::user()->id);
$tokens = $user->generateAccountTokens();
Mail::to($user)->send(new UserAccountDeleteConfirmation($user, $tokens, $request->ip()));
$user->delete();
Auth::logout();
$request->session()->flash('success', 'Please check your email to finish deleting your account.');
return redirect()->to('/');
}
public function processDeleteConfirmation(Request $request, $ID, $action, $token)
{
// We can't rely on Laravel's route model injection, because it'll ignore soft-deleted models,
// so we have to use a special scope to find them ourselves.
$user = User::withTrashed()->findOrFail($ID);
$email = $user->email;
switch($action)
{
case 'confirm':
if ($user->verifyAccountToken($token, 'deleteToken'))
{
Log::info('SECURITY: User deleted account!', [
'confirmDeleteToken' => $token,
'ipAddress' => $request->ip(),
'email' => $user->email
]);
$user->forceDelete();
$request->session()->flash('success', 'Account permanently deleted. Thank you for using our service.');
return redirect()->to('/');
}
break;
case 'cancel':
if ($user->verifyAccountToken($token, 'cancelToken'))
{
$user->restore();
$request->session()->flash('success', 'Account deletion cancelled! You may now login.');
return redirect()->to(route('login'));
}
break;
default:
abort(404, 'The page you were trying to access may not exist or may be expired.');
}
}
}

View File

@@ -2,15 +2,18 @@
namespace App;
use App\Traits\HandlesAccountTokens;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Mpociot\Teamwork\Traits\UserHasTeams;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements MustVerifyEmail
{
use UserHasTeams, Notifiable, HasRoles;
use UserHasTeams, Notifiable, HasRoles, SoftDeletes, HandlesAccountTokens;
/**
* The attributes that are mass assignable.
@@ -74,7 +77,6 @@ class User extends Authenticatable implements MustVerifyEmail
public function isStaffMember()
{
return $this->hasAnyRole('reviewer', 'admin', 'hiringManager');
@@ -85,8 +87,7 @@ class User extends Authenticatable implements MustVerifyEmail
return !is_null($this->twofa_secret);
}
public function routeNotificationForSlack($notification)
{
return config('slack.webhook.integrationURL');

View File

@@ -13,7 +13,7 @@ use GrahamCampbell\Markdown\Facades\Markdown;
class Vacancy extends Model
{
use UsedByTeams;
//use UsedByTeams;
public $fillable = [
@@ -25,7 +25,8 @@ class Vacancy extends Model
'vacancyFormID',
'vacancyCount',
'vacancyStatus',
'vacancySlug'
'vacancySlug',
'team_id'
];
@@ -49,6 +50,12 @@ class Vacancy extends Model
}
public function teams()
{
return $this->belongsToMany('App\Team', 'team_has_vacancy');
}
public function forms()
{
return $this->belongsTo('App\Form', 'vacancyFormID', 'id');