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_NAME=Laravel
APP_ENV=local APP_ENV=local
APP_KEY=base64:47yorkyoH3qCrKKO4eG6LpZUogoTC51qey5vYq/O3AM= APP_KEY=base64:XP4FSfZLrj3n2MbhbOVWp4ldCbU0Ew+bhiEpHyOpxVw=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
APP_DEMO=false APP_DEMO=false

View File

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

View File

@@ -20,7 +20,7 @@ class CreateTeamFolderInvitationsTable extends Migration
$table->text('email'); $table->text('email');
$table->string('color')->nullable(); $table->string('color')->nullable();
$table->enum('permission', ['can-edit', 'can-view', 'can-view-and-download']); $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->timestamps();
$table->charset = 'utf8mb4'; $table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci'; $table->collation = 'utf8mb4_unicode_ci';

View File

@@ -6,22 +6,37 @@
v-if="invitation" v-if="invitation"
:title="$t('Invitation To Join Team Folder')" :title="$t('Invitation To Join Team Folder')"
:description=" :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, name: invitation.data.relationships.inviter.data.attributes.name,
}) })
" "
> >
<div class="relative mx-auto mb-10 w-24 text-center"> <div class="relative mx-auto mb-10 w-24 text-center">
<VueFolderTeamIcon class="inline-block w-28" /> <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> </div>
</Headline> </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 <AuthButton
@click.native="acceptInvitation" @click.native="acceptInvitation"
class="mb-12 w-full justify-center md:w-min" class="mb-12 w-full justify-center md:w-min"
icon="chevron-right" icon="chevron-right"
:text="$t('Accept Invitation')" :text="acceptButton"
:loading="isLoading" :loading="isLoading"
:disabled="isLoading" :disabled="isLoading"
/> />
@@ -37,10 +52,17 @@
<!--Accepted invitation screen--> <!--Accepted invitation screen-->
<AuthContent v-if="invitation" name="accepted" :visible="false"> <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' }"> <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>
<router-link <router-link
@@ -51,29 +73,47 @@
params: { id: invitation.data.attributes.parent_id }, 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> </router-link>
</AuthContent> </AuthContent>
<!--Denied invitation screen--> <!--Denied invitation screen-->
<AuthContent name="denied" :visible="false"> <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' }"> <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> </router-link>
</AuthContent> </AuthContent>
<!--Used or Expired invitation screen--> <!--Used or Expired invitation screen-->
<AuthContent name="expired" :visible="false"> <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' }"> <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')" /> <AuthButton class="mb-12 w-full justify-center md:w-min" icon="chevron-right" :text="$t('Log In')" />
</router-link> </router-link>
<router-link replace v-if="config.isAuthenticated" :to="{ name: 'SharedWithMe' }"> <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> </router-link>
</AuthContent> </AuthContent>
</AuthContentWrapper> </AuthContentWrapper>
@@ -106,6 +146,11 @@ export default {
}, },
computed: { computed: {
...mapGetters(['config']), ...mapGetters(['config']),
acceptButton() {
return this.invitation && this.invitation.data.attributes.isExistedUser
? this.$t('Accept Invitation')
: this.$t('Accept and Register Account')
},
}, },
data() { data() {
return { return {
@@ -121,7 +166,12 @@ export default {
axios axios
.put(`/api/teams/invitations/${this.$router.currentRoute.params.id}`) .put(`/api/teams/invitations/${this.$router.currentRoute.params.id}`)
.then(() => { .then(() => {
this.goToAuthPage('accepted')
if (this.invitation.data.attributes.isExistedUser) {
this.goToAuthPage('accepted')
} else {
this.$router.push({name: 'SignUp'})
}
}) })
.catch(() => { .catch(() => {
this.$isSomethingWrong() this.$isSomethingWrong()

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
<?php <?php
namespace App\Users\Actions; namespace App\Users\Actions;
use App\Users\Models\User; use App\Users\Models\User;
use App\Users\DTO\CreateUserData; use App\Users\DTO\CreateUserData;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Domain\Teams\Models\TeamFolderInvitation;
use Domain\Teams\Models\TeamFolderMember;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
class CreateNewUserAction extends Controller class CreateNewUserAction extends Controller
{ {
public function __construct( public function __construct(
protected AutoSubscribeForMeteredBillingAction $autoSubscribeForMeteredBilling, protected AutoSubscribeForMeteredBillingAction $autoSubscribeForMeteredBilling,
) { ) {}
}
/** /**
* Validate and create a new user. * Validate and create a new user.
@@ -39,13 +41,27 @@ class CreateNewUserAction extends Controller
'avatar' => $data->avatar, '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 // Subscribe user for metered billing
if ($settings['subscription_type'] === 'metered') { if ($settings['subscription_type'] === 'metered') {
($this->autoSubscribeForMeteredBilling)($user); ($this->autoSubscribeForMeteredBilling)($user);
} }
// Mark as verified if verification is disabled // Mark as verified if verification is disabled
if (! $data->password || ! intval($settings['user_verification'])) { if (!$data->password || !intval($settings['user_verification'])) {
$user->markEmailAsVerified(); $user->markEmailAsVerified();
} }

View File

@@ -32,7 +32,7 @@ class RegisterUserRequest extends FormRequest
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email', new EmailProvider], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email', new EmailProvider],
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'password' => $this->passwordRules(), '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

@@ -81,8 +81,9 @@ class StoreEnvironmentSettingsController extends Controller
'APP_DEBUG' => 'false', 'APP_DEBUG' => 'false',
], ],
'local' => [ 'local' => [
'APP_ENV' => 'local', 'APP_ENV' => 'local',
'APP_DEBUG' => 'true', 'APP_DEBUG' => 'true',
'QUEUE_CONNECTION' => 'sync',
], ],
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ class TeamManagementTest extends TestCase
/** /**
* @test * @test
*/ */
public function it_accept_team_folder_invite() public function it_accept_team_folder_invite_as_registered_user()
{ {
$member = User::factory() $member = User::factory()
->create([ ->create([
@@ -59,7 +59,7 @@ class TeamManagementTest extends TestCase
'parent_id' => $folder->id, 'parent_id' => $folder->id,
'email' => $member->email, 'email' => $member->email,
'status' => 'pending', 'status' => 'pending',
'permission' => 'can-edit', 'permission' => 'can-view',
]); ]);
$this $this
@@ -75,8 +75,69 @@ class TeamManagementTest extends TestCase
->assertDatabaseHas('team_folder_members', [ ->assertDatabaseHas('team_folder_members', [
'parent_id' => $folder->id, 'parent_id' => $folder->id,
'user_id' => $member->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', '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,
]);
} }
/** /**