Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);

namespace App\Athenia\Contracts\Repositories\User;

use App\Athenia\Contracts\Repositories\BaseRepositoryContract;
use App\Athenia\Models\User\InvitationToken;

/**
* Interface InvitationTokenRepositoryContract
* @package App\Contracts\Repositories\User
*/
interface InvitationTokenRepositoryContract extends BaseRepositoryContract
{
/**
* Generates a unique token, or throws an exception if it cannot do so.
*
* @throws \OverflowException
* @return string
*/
public function generateUniqueToken(): string;

/**
* Finds an invitation token by its token string
*
* @param string $token
* @return InvitationToken|null
*/
public function findByToken(string $token): ?InvitationToken;
}
51 changes: 51 additions & 0 deletions code/app/Athenia/Events/User/InvitationAcceptedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);

namespace App\Athenia\Events\User;

use App\Athenia\Models\User\InvitationToken;
use App\Models\User\User;

/**
* Class InvitationAcceptedEvent
* @package App\Events\User
*/
class InvitationAcceptedEvent
{
/**
* @var User
*/
private User $user;

/**
* @var InvitationToken
*/
private InvitationToken $invitationToken;

/**
* InvitationAcceptedEvent constructor.
* @param User $user
* @param InvitationToken $invitationToken
*/
public function __construct(User $user, InvitationToken $invitationToken)
{
$this->user = $user;
$this->invitationToken = $invitationToken;
}

/**
* @return User
*/
public function getUser(): User
{
return $this->user;
}

/**
* @return InvitationToken
*/
public function getInvitationToken(): InvitationToken
{
return $this->invitationToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

namespace App\Athenia\Http\Core\Controllers;

use App\Athenia\Contracts\Repositories\User\InvitationTokenRepositoryContract;
use App\Athenia\Contracts\Repositories\User\UserRepositoryContract;
use App\Athenia\Events\User\InvitationAcceptedEvent;
use App\Athenia\Events\User\SignUpEvent;
use App\Http\Core\Requests;
use App\Models\User\User;
Expand Down Expand Up @@ -51,19 +53,26 @@ abstract class AuthenticationControllerAbstract extends BaseControllerAbstract
*/
protected $dispatcher;

/**
* @var InvitationTokenRepositoryContract
*/
protected $invitationTokenRepository;

/**
* AuthenticationController constructor.
* @param UserRepositoryContract $userRepository
* @param Hasher $hasher
* @param JWTAuth $auth
* @param Dispatcher $dispatcher
* @param InvitationTokenRepositoryContract $invitationTokenRepository
*/
public function __construct(UserRepositoryContract $userRepository, Hasher $hasher, JWTAuth $auth, Dispatcher $dispatcher)
public function __construct(UserRepositoryContract $userRepository, Hasher $hasher, JWTAuth $auth, Dispatcher $dispatcher, InvitationTokenRepositoryContract $invitationTokenRepository)
{
$this->userRepository = $userRepository;
$this->hasher = $hasher;
$this->auth = $auth;
$this->dispatcher = $dispatcher;
$this->invitationTokenRepository = $invitationTokenRepository;
}

/**
Expand Down Expand Up @@ -243,6 +252,10 @@ public function signUp(Requests\Authentication\SignUpRequest $request)
{
$data = $request->json()->all();

// Store invitation token separately and remove it from user data
$invitationTokenValue = $data['invitation_token'] ?? null;
unset($data['invitation_token']);

$forcedData = [
'password' => $this->hasher->make($data['password']),
];
Expand All @@ -252,6 +265,14 @@ public function signUp(Requests\Authentication\SignUpRequest $request)

$this->dispatcher->dispatch(new SignUpEvent($model));

// If an invitation token was provided, dispatch the InvitationAcceptedEvent
if ($invitationTokenValue) {
$invitationToken = $this->invitationTokenRepository->findByToken($invitationTokenValue);
if ($invitationToken) {
$this->dispatcher->dispatch(new InvitationAcceptedEvent($model, $invitationToken));
}
}

$token = $this->auth->fromUser($model);
return new JsonResponse([
'token' => $token,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

use App\Athenia\Http\Core\Requests\BaseUnauthenticatedRequest;
use App\Athenia\Http\Core\Requests\Traits\HasNoExpands;
use App\Athenia\Validators\InvitationTokenIsValidValidator;
use App\Models\User\User;
use Illuminate\Contracts\Config\Repository;

/**
* Class SignUpRequest
Expand All @@ -19,15 +21,23 @@ class SignUpRequest extends BaseUnauthenticatedRequest
* Gets the rules for the verification
*
* @param User $user
* @param Repository $config
* @return array
*/
public function rules(User $user)
public function rules(User $user, Repository $config)
{
return [
$rules = [
'email' => 'required|string|max:120|email|unique:users,email',
'first_name' => 'required|string|max:120',
'last_name' => 'string|max:120',
'password' => 'required|string|min:6|max:256',
];

// Add invitation token validation if invitations are required
if ($config->get('athenia.invitation_required', false)) {
$rules['invitation_token'] = 'required|string|' . InvitationTokenIsValidValidator::KEY;
}

return $rules;
}
}
51 changes: 51 additions & 0 deletions code/app/Athenia/Listeners/User/InvitationAcceptedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);

namespace App\Athenia\Listeners\User;

use App\Athenia\Contracts\Repositories\User\InvitationTokenRepositoryContract;
use App\Athenia\Events\User\InvitationAcceptedEvent;
use Illuminate\Support\Carbon;

/**
* Class InvitationAcceptedListener
* @package App\Athenia\Listeners\User
*/
class InvitationAcceptedListener
{
/**
* @var InvitationTokenRepositoryContract
*/
private InvitationTokenRepositoryContract $invitationTokenRepository;

/**
* InvitationAcceptedListener constructor.
* @param InvitationTokenRepositoryContract $invitationTokenRepository
*/
public function __construct(InvitationTokenRepositoryContract $invitationTokenRepository)
{
$this->invitationTokenRepository = $invitationTokenRepository;
}

/**
* Handles the invitation accepted event by marking the token as used
* and adding the associated role to the user if present
*
* @param InvitationAcceptedEvent $event
*/
public function handle(InvitationAcceptedEvent $event): void
{
$user = $event->getUser();
$invitationToken = $event->getInvitationToken();

// Mark the invitation token as used
$this->invitationTokenRepository->update($invitationToken, [
'used_at' => Carbon::now(),
]);

// If the invitation token has a role, add it to the user
if ($invitationToken->role_id) {
$user->roles()->attach($invitationToken->role_id);
}
}
}
100 changes: 100 additions & 0 deletions code/app/Athenia/Models/User/InvitationToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);

namespace App\Athenia\Models\User;

use App\Athenia\Models\BaseModelAbstract;
use App\Athenia\Models\Role;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
* Class InvitationToken
*
* @property int $id
* @property string $token
* @property int|null $role_id
* @property \Illuminate\Support\Carbon|null $used_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property mixed|null $created_at
* @property mixed|null $updated_at
* @property-read \App\Athenia\Models\Role|null $role
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|\App\Models\User\InvitationToken newModelQuery()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|\App\Models\User\InvitationToken newQuery()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder|\App\Models\User\InvitationToken query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereToken($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereRoleId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereUsedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Models\User\InvitationToken whereUpdatedAt($value)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken getAggregateMethod()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken isAppendRelationsCount()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken isLeftJoin()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken isUseTableAlias()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken joinRelations($relations, $leftJoin = null)
* @method static \Illuminate\Database\Eloquent\Builder<static>|InvitationToken onlyTrashed()
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken orWhereInJoin($column, $values)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken orWhereJoin($column, $operator, $value)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken orWhereNotInJoin($column, $values)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken orderByJoin($column, $direction = 'asc', $aggregateMethod = null)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken setAggregateMethod(string $aggregateMethod)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken setAppendRelationsCount(bool $appendRelationsCount)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken setLeftJoin(bool $leftJoin)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken setUseTableAlias(bool $useTableAlias)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken whereInJoin($column, $values, $boolean = 'and', $not = false)
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken whereJoin($column, $operator, $value, $boolean = 'and')
* @method static \AdminUI\Laravel\EloquentJoin\EloquentJoinBuilder<static>|InvitationToken whereNotInJoin($column, $values, $boolean = 'and')
* @method static \Illuminate\Database\Eloquent\Builder<static>|InvitationToken withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|InvitationToken withoutTrashed()
* @mixin \Eloquent
*/
class InvitationToken extends BaseModelAbstract
{
/**
* The role relation for this invitation token
*
* @return BelongsTo
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}

/**
* Check if this invitation token has been used
*
* @return bool
*/
public function isUsed(): bool
{
return $this->used_at !== null;
}

/**
* Swagger definition below for an invitation token...
*
* @SWG\Definition(
* type="object",
* definition="InvitationToken",
* @SWG\Property(
* property="token",
* type="string",
* maxLength=40,
* description="The invitation token that was generated."
* ),
* @SWG\Property(
* property="role_id",
* type="integer",
* format="int32",
* description="The role ID that will be assigned when this invitation is accepted."
* ),
* @SWG\Property(
* property="used_at",
* type="string",
* format="date-time",
* description="UTC date of when this invitation was used."
* ),
* )
*/
}
5 changes: 5 additions & 0 deletions code/app/Athenia/Providers/BaseEventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Athenia\Events\Payment\PaymentReversedEvent;
use App\Athenia\Events\User\Contact\ContactCreatedEvent;
use App\Athenia\Events\User\ForgotPasswordEvent;
use App\Athenia\Events\User\InvitationAcceptedEvent;
use App\Athenia\Events\User\SignUpEvent;
use App\Athenia\Events\User\UserMergeEvent;
use App\Athenia\Events\Vote\VoteCreatedEvent;
Expand All @@ -22,6 +23,7 @@
use App\Athenia\Listeners\Messaging\MessageSentListener;
use App\Athenia\Listeners\Payment\DefaultPaymentMethodSetListener;
use App\Athenia\Listeners\User\ForgotPasswordListener;
use App\Athenia\Listeners\User\InvitationAcceptedListener;
use App\Athenia\Listeners\User\UserMerge\UserBallotCompletionsMergeListener;
use App\Athenia\Listeners\User\UserMerge\UserCreatedArticlesMergeListener;
use App\Athenia\Listeners\User\UserMerge\UserCreatedIterationsMergeListener;
Expand Down Expand Up @@ -71,6 +73,9 @@ public function listens(): array
ForgotPasswordEvent::class => [
ForgotPasswordListener::class,
],
InvitationAcceptedEvent::class => [
InvitationAcceptedListener::class,
],
MessageCreatedEvent::class => [
MessageCreatedListener::class,
],
Expand Down
Loading