handle team invitation for non registered user

This commit is contained in:
Čarodej
2022-02-12 11:28:08 +01:00
parent 4498461e70
commit 00c6562719
18 changed files with 216 additions and 51 deletions

View File

@@ -1,6 +1,6 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:47yorkyoH3qCrKKO4eG6LpZUogoTC51qey5vYq/O3AM=
APP_KEY=base64:XP4FSfZLrj3n2MbhbOVWp4ldCbU0Ew+bhiEpHyOpxVw=
APP_DEBUG=true
APP_URL=http://localhost
APP_DEMO=false

View File

@@ -95,6 +95,10 @@ return [
'name' => 'allowed_adsense',
'value' => 0,
],
[
'name' => 'allowed_recaptcha',
'value' => 0,
],
],
'extended' => [
[
@@ -177,6 +181,10 @@ return [
'name' => 'allowed_adsense',
'value' => 0,
],
[
'name' => 'allowed_recaptcha',
'value' => 0,
],
// Subscription
[

View File

@@ -20,7 +20,7 @@ class CreateTeamFolderInvitationsTable extends Migration
$table->text('email');
$table->string('color')->nullable();
$table->enum('permission', ['can-edit', 'can-view', 'can-view-and-download']);
$table->enum('status', ['pending', 'accepted', 'rejected'])->default('pending');
$table->enum('status', ['pending', 'accepted', 'waiting-for-registration', 'rejected'])->default('pending');
$table->timestamps();
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';

View File

@@ -6,22 +6,37 @@
v-if="invitation"
:title="$t('Invitation To Join Team Folder')"
:description="
$t('{name} invite you to join with his team into shared team folder', {
$t('Jane invite you to join with his team into shared team folder', {
name: invitation.data.relationships.inviter.data.attributes.name,
})
"
>
<div class="relative mx-auto mb-10 w-24 text-center">
<VueFolderTeamIcon class="inline-block w-28" />
<MemberAvatar :member="invitation.data.relationships.inviter" class="absolute -bottom-2.5 -right-6" :is-border="true" :size="38" />
<MemberAvatar
:member="invitation.data.relationships.inviter"
class="absolute -bottom-2.5 -right-6"
:is-border="true"
:size="38"
/>
</div>
</Headline>
<p
v-if="invitation"
class="mx-auto mb-4 max-w-md text-sm text-gray-500"
v-html="
$t('Register account with your email peterpapp@makingcg.com and get access to this Team Folder.', {
email: invitation.data.attributes.email,
})
"
></p>
<AuthButton
@click.native="acceptInvitation"
class="mb-12 w-full justify-center md:w-min"
icon="chevron-right"
:text="$t('Accept Invitation')"
:text="acceptButton"
:loading="isLoading"
:disabled="isLoading"
/>
@@ -37,10 +52,17 @@
<!--Accepted invitation screen-->
<AuthContent v-if="invitation" name="accepted" :visible="false">
<Headline :title="$t('You are successfully joined')" :description="$t('You can now proceed to your account and participate in team folder')" />
<Headline
:title="$t('You are successfully joined')"
:description="$t('You can now proceed to your account and participate in team folder')"
/>
<router-link replace v-if="!config.isAuthenticated" :to="{ name: 'SignIn' }">
<AuthButton class="mb-12 w-full justify-center md:w-min" icon="chevron-right" :text="$t('Proceed to your account')" />
<AuthButton
class="mb-12 w-full justify-center md:w-min"
icon="chevron-right"
:text="$t('Proceed to your account')"
/>
</router-link>
<router-link
@@ -51,29 +73,47 @@
params: { id: invitation.data.attributes.parent_id },
}"
>
<AuthButton class="mb-12 w-full justify-center md:w-min" icon="chevron-right" :text="$t('Go to Team Folder')" />
<AuthButton
class="mb-12 w-full justify-center md:w-min"
icon="chevron-right"
:text="$t('Go to Team Folder')"
/>
</router-link>
</AuthContent>
<!--Denied invitation screen-->
<AuthContent name="denied" :visible="false">
<Headline :title="$t('You are successfully denied invitation')" :description="$t('You can now proceed to your account')" />
<Headline
:title="$t('You are successfully denied invitation')"
:description="$t('You can now proceed to your account')"
/>
<router-link :to="{ name: 'SignIn' }">
<AuthButton class="mb-12 w-full justify-center md:w-min" icon="chevron-right" :text="$t('Proceed to your account')" />
<AuthButton
class="mb-12 w-full justify-center md:w-min"
icon="chevron-right"
:text="$t('Proceed to your account')"
/>
</router-link>
</AuthContent>
<!--Used or Expired invitation screen-->
<AuthContent name="expired" :visible="false">
<Headline :title="$t('Your invitation has been used')" :description="$t('We are sorry but this invitation was used previously')" />
<Headline
:title="$t('Your invitation has been used')"
:description="$t('We are sorry but this invitation was used previously')"
/>
<router-link replace v-if="!config.isAuthenticated" :to="{ name: 'SignIn' }">
<AuthButton class="mb-12 w-full justify-center md:w-min" icon="chevron-right" :text="$t('Log In')" />
</router-link>
<router-link replace v-if="config.isAuthenticated" :to="{ name: 'SharedWithMe' }">
<AuthButton class="mb-12 w-full justify-center md:w-min" icon="chevron-right" :text="$t('Go to your shared folders')" />
<AuthButton
class="mb-12 w-full justify-center md:w-min"
icon="chevron-right"
:text="$t('Go to your shared folders')"
/>
</router-link>
</AuthContent>
</AuthContentWrapper>
@@ -106,6 +146,11 @@ export default {
},
computed: {
...mapGetters(['config']),
acceptButton() {
return this.invitation && this.invitation.data.attributes.isExistedUser
? this.$t('Accept Invitation')
: this.$t('Accept and Register Account')
},
},
data() {
return {
@@ -121,7 +166,12 @@ export default {
axios
.put(`/api/teams/invitations/${this.$router.currentRoute.params.id}`)
.then(() => {
if (this.invitation.data.attributes.isExistedUser) {
this.goToAuthPage('accepted')
} else {
this.$router.push({name: 'SignUp'})
}
})
.catch(() => {
this.$isSomethingWrong()

View File

@@ -1090,6 +1090,10 @@ class SetupDevEnvironment extends Command
'name' => 'subscription_type',
'value' => 'fixed',
],
[
'name' => 'allowed_recaptcha',
'value' => 0,
],
])->each(function ($col) {
Setting::updateOrCreate([
'name' => $col['name'],

View File

@@ -187,6 +187,10 @@ class SetupProdEnvironment extends Command
'name' => 'billing_vat_number',
'value' => null,
],
[
'name' => 'allowed_recaptcha',
'value' => 0,
],
])->each(function ($col) {
Setting::forceCreate([
'name' => $col['name'],

View File

@@ -1,17 +1,19 @@
<?php
namespace App\Users\Actions;
use App\Users\Models\User;
use App\Users\DTO\CreateUserData;
use App\Http\Controllers\Controller;
use Domain\Teams\Models\TeamFolderInvitation;
use Domain\Teams\Models\TeamFolderMember;
use Illuminate\Auth\Events\Registered;
class CreateNewUserAction extends Controller
{
public function __construct(
protected AutoSubscribeForMeteredBillingAction $autoSubscribeForMeteredBilling,
) {
}
) {}
/**
* Validate and create a new user.
@@ -39,13 +41,27 @@ class CreateNewUserAction extends Controller
'avatar' => $data->avatar,
]);
// Join to previously accepted team folder invitations
TeamFolderInvitation::where('email', $user->email)
->where('status', 'waiting-for-registration')
->cursor()
->each(function ($invitation) use ($user) {
TeamFolderMember::create([
'user_id' => $user->id,
'parent_id' => $invitation->parent_id,
'permission' => $invitation->permission,
]);
$invitation->accept();
});
// Subscribe user for metered billing
if ($settings['subscription_type'] === 'metered') {
($this->autoSubscribeForMeteredBilling)($user);
}
// Mark as verified if verification is disabled
if (! $data->password || ! intval($settings['user_verification'])) {
if (!$data->password || !intval($settings['user_verification'])) {
$user->markEmailAsVerified();
}

View File

@@ -32,7 +32,7 @@ class RegisterUserRequest extends FormRequest
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email', new EmailProvider],
'name' => 'required|string|max:255',
'password' => $this->passwordRules(),
'reCaptcha' => [new RequiredIf(get_settings('allowed_recaptcha') == 1), 'string', app(ReCaptchaRules::class)],
'reCaptcha' => [new RequiredIf(get_settings('allowed_recaptcha') == 1), 'string', 'nullable', app(ReCaptchaRules::class)],
];
}
}

View File

@@ -83,6 +83,7 @@ class StoreEnvironmentSettingsController extends Controller
'local' => [
'APP_ENV' => 'local',
'APP_DEBUG' => 'true',
'QUEUE_CONNECTION' => 'sync',
],
];

View File

@@ -2,6 +2,7 @@
namespace Domain\Teams\Controllers;
use App\Users\Models\User;
use Domain\Teams\Models\TeamFolderMember;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
@@ -13,7 +14,7 @@ class InvitationsController extends Controller
{
public function show(TeamFolderInvitation $invitation)
{
if ($invitation->status === 'accepted') {
if ($invitation->status !== 'pending') {
abort(410);
}
@@ -23,19 +24,24 @@ class InvitationsController extends Controller
public function update(
TeamFolderInvitation $invitation
): ResponseFactory | Response {
$user = User::where('email', $invitation->email)
->firstOrFail();
$user = User::where('email', $invitation->email);
$invitation->update([
'status' => 'accepted',
]);
if ($user->exists()) {
$invitation->accept();
DB::table('team_folder_members')
->insert([
// Store team member
TeamFolderMember::create([
'user_id' => $user->first()->id,
'parent_id' => $invitation->parent_id,
'user_id' => $user->id,
'permission' => 'can-edit',
'permission' => $invitation->permission,
]);
}
if ($user->doesntExist()) {
$invitation->update([
'status' => 'waiting-for-registration',
]);
}
return response('Done', 204);
}
@@ -43,9 +49,7 @@ class InvitationsController extends Controller
public function destroy(
TeamFolderInvitation $invitation
): ResponseFactory | Response {
$invitation->update([
'status' => 'rejected',
]);
$invitation->reject();
return response('Done', 204);
}

View File

@@ -31,6 +31,18 @@ class TeamFolderInvitation extends Model
protected $keyType = 'string';
public function accept() {
$this->update([
'status' => 'accepted',
]);
}
public function reject() {
$this->update([
'status' => 'rejected',
]);
}
protected static function newFactory(): TeamFolderInvitationFactory
{
return TeamFolderInvitationFactory::new();

View File

@@ -1,6 +1,7 @@
<?php
namespace Domain\Teams\Resources;
use App\Users\Models\User;
use Illuminate\Http\Resources\Json\JsonResource;
class TeamInvitationResource extends JsonResource
@@ -17,6 +18,7 @@ class TeamInvitationResource extends JsonResource
'color' => $this->color,
'status' => $this->status,
'permission' => $this->permission,
'isExistedUser' => User::where('email', $this->email)->exists(),
],
'relationships' => [
$this->mergeWhen($this->inviter, fn () => [

View File

@@ -41,7 +41,7 @@ class HomepageTest extends TestCase
{
$this->get('/')
->assertStatus(200)
->assertSee('setup-disclaimer')
->assertSee('installation-needed')
->assertSee('VueFileManager');
}

View File

@@ -190,12 +190,12 @@ class SettingsTest extends TestCase
$this
->actingAs($admin)
->postJson('/api/admin/settings/email', [
'driver' => 'smtp',
'host' => 'smtp.email.com',
'port' => 25,
'username' => 'john@doe.com',
'password' => 'secret',
'encryption' => 'tls',
'mailDriver' => 'smtp',
'smtp.host' => 'smtp.email.com',
'smtp.port' => 25,
'smtp.username' => 'john@doe.com',
'smtp.password' => 'secret',
'smtp.encryption' => 'tls',
])->assertStatus(204);
}
}

View File

@@ -21,7 +21,7 @@ class SetupWizardTest extends TestCase
$this->postJson('/api/setup/purchase-code', [
'purchaseCode' => '8624194e-3156-4cd0-944e-3440fcecdacb',
])->assertStatus(204);
])->assertStatus(201);
}
/**

View File

@@ -1,4 +1,5 @@
<?php
namespace Tests\Domain\Sharing;
use Tests\TestCase;
@@ -193,7 +194,7 @@ class VisitorBrowseTest extends TestCase
}
// Check public shared item
if (! $is_protected) {
if (!$is_protected) {
$this->getJson("/api/browse/folders/$root->id/$share->token")
->assertStatus(200)
->assertJsonFragment([
@@ -260,9 +261,10 @@ class VisitorBrowseTest extends TestCase
$tree = [
[
'id' => $share->item_id,
'name' => 'Home',
'location' => 'public',
'isMovable' => true,
'isOpen' => true,
'folders' => [
[
'id' => $folder_level_2->id,
@@ -308,7 +310,7 @@ class VisitorBrowseTest extends TestCase
}
// Check public shared item
if (! $is_protected) {
if (!$is_protected) {
$this->getJson("/api/browse/navigation/$share->token")
->assertStatus(200)
->assertExactJson($tree);
@@ -360,7 +362,7 @@ class VisitorBrowseTest extends TestCase
}
// Check public shared item
if (! $is_protected) {
if (!$is_protected) {
$this->getJson("/api/browse/search/$share->token?query=doc")
->assertStatus(200)
->assertJsonFragment([
@@ -411,7 +413,7 @@ class VisitorBrowseTest extends TestCase
}
// Check public shared item
if (! $is_protected) {
if (!$is_protected) {
$this->getJson("/api/browse/search/$share->token?query=doc")
->assertStatus(200)
->assertJsonFragment([]);
@@ -458,7 +460,7 @@ class VisitorBrowseTest extends TestCase
}
// Check public shared item
if (! $is_protected) {
if (!$is_protected) {
$this->getJson("/api/browse/file/$share->token")
->assertStatus(200)
->assertJsonFragment([

View File

@@ -300,6 +300,7 @@ class VisitorManipulatingTest extends TestCase
collect([true, false])
->each(function ($is_protected) {
$user = User::factory()
->hasSettings()
->create();
$folder = Folder::factory(Folder::class)

View File

@@ -44,7 +44,7 @@ class TeamManagementTest extends TestCase
/**
* @test
*/
public function it_accept_team_folder_invite()
public function it_accept_team_folder_invite_as_registered_user()
{
$member = User::factory()
->create([
@@ -59,7 +59,7 @@ class TeamManagementTest extends TestCase
'parent_id' => $folder->id,
'email' => $member->email,
'status' => 'pending',
'permission' => 'can-edit',
'permission' => 'can-view',
]);
$this
@@ -75,8 +75,69 @@ class TeamManagementTest extends TestCase
->assertDatabaseHas('team_folder_members', [
'parent_id' => $folder->id,
'user_id' => $member->id,
'permission' => 'can-view',
]);
}
/**
* @test
*/
public function it_accept_team_folder_invite_as_guest_user()
{
$folder = Folder::factory()
->create();
$invitation = TeamFolderInvitation::factory()
->create([
'parent_id' => $folder->id,
'email' => 'howdy@hi5ve.digital',
'status' => 'pending',
'permission' => 'can-edit',
]);
$this
->putJson("/api/teams/invitations/{$invitation->id}")
->assertNoContent();
$this
->assertDatabaseHas('team_folder_invitations', [
'parent_id' => $folder->id,
'status' => 'waiting-for-registration',
])
->assertDatabaseMissing('team_folder_members', [
'parent_id' => $folder->id,
'permission' => 'can-edit',
]);
}
/**
* @test
*/
public function it_apply_accepted_invitation_after_user_registration()
{
$invitation = TeamFolderInvitation::factory()
->create([
'email' => 'john@doe.com',
'status' => 'waiting-for-registration',
]);
$this->postJson('api/register', [
'email' => 'john@doe.com',
'password' => 'SecretPassword',
'password_confirmation' => 'SecretPassword',
'name' => 'John Doe',
])->assertStatus(201);
$this
->assertDatabaseHas('team_folder_invitations', [
'parent_id' => $invitation->parent_id,
'status' => 'accepted',
])
->assertDatabaseHas('team_folder_members', [
'parent_id' => $invitation->parent_id,
'user_id' => User::first()->id,
'permission' => $invitation->permission,
]);
}
/**