Team functionality (#459)
* add api enpoints for adding, removing, updating user to workspace and leaving workspace * feat: updates client site workspace settings * refactor and add domain setting ui in modal * move workspace user functionality to its own component * adds tests * fix linting * updates select input to FlatSelectInput * moves workspace user role edit to seperated component * move user adding to its own component * adds check to usure users exist before checking is admin * fix loading users * feat: invite user to team functionality * fix token coulmn * fix self host mode changes * tests for user invite * Refactor back-end * Rename variables * Improve some styling elements + refactor workspace settings * More styling * More UI polishing * More UI fixes * PHP linting * Implemented most of the logic for team-functionnality * Fix user avatar URL * WIP remove users on cancellation * Finished pricing for team functionality * Fix tests * Fix linting * Added pricing_enabled helper * Fix pricing_enabled shortcut * Debug CI * Disable pricing when testing --------- Co-authored-by: LL-Etiane <lukongleinyuyetiane@gmail.com> Co-authored-by: Lukong Etiane <83535251+LL-Etiane@users.noreply.github.com> Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace App\Models\Billing;
|
||||
|
||||
use App\Events\SubscriptionCreated;
|
||||
use App\Events\Billing\SubscriptionCreated;
|
||||
use App\Events\Billing\SubscriptionUpdated;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Laravel\Cashier\Subscription as CashierSubscription;
|
||||
|
||||
@@ -12,6 +13,7 @@ class Subscription extends CashierSubscription
|
||||
|
||||
protected $dispatchesEvents = [
|
||||
'created' => SubscriptionCreated::class,
|
||||
'updated' => SubscriptionUpdated::class,
|
||||
];
|
||||
|
||||
public static function booted(): void
|
||||
|
||||
@@ -8,8 +8,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class License extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_INACTIVE = 'inactive';
|
||||
|
||||
protected $fillable = [
|
||||
@@ -32,6 +32,11 @@ class License extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isActive()
|
||||
{
|
||||
return $this->status === self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_ACTIVE);
|
||||
@@ -55,6 +60,15 @@ class License extends Model
|
||||
][$this->meta['tier']];
|
||||
}
|
||||
|
||||
public function getMaxUsersLimitCountAttribute(): ?int
|
||||
{
|
||||
return [
|
||||
1 => 1,
|
||||
2 => 5,
|
||||
3 => null,
|
||||
][$this->meta['tier']];
|
||||
}
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::saved(function (License $license) {
|
||||
|
||||
@@ -17,6 +17,9 @@ class User extends Authenticatable implements JWTSubject
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
|
||||
public const ROLE_ADMIN = 'admin';
|
||||
public const ROLE_USER = 'user';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
@@ -80,7 +83,7 @@ class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
return vsprintf('https://www.gravatar.com/avatar/%s.jpg?s=200&d=%s', [
|
||||
md5(strtolower($this->email)),
|
||||
$this->name ? urlencode("https://ui-avatars.com/api/$this->name") : 'mp',
|
||||
$this->name ? urlencode("https://ui-avatars.com/api/$this->name.jpg") : 'mp',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -235,6 +238,8 @@ class User extends Authenticatable implements JWTSubject
|
||||
foreach ($user->workspaces as $workspace) {
|
||||
if ($workspace->users()->count() == 1) {
|
||||
$workspace->delete();
|
||||
} else {
|
||||
$workspace->users()->detach($user->id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
87
app/Models/UserInvite.php
Normal file
87
app/Models/UserInvite.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\Billing\WorkspaceUsersUpdated;
|
||||
use App\Mail\UserInvitationEmail;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserInvite extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const PENDING_STATUS = 'pending';
|
||||
public const ACCEPTED_STATUS = 'accepted';
|
||||
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'role',
|
||||
'workspace_id',
|
||||
'valid_until',
|
||||
'status',
|
||||
'token',
|
||||
];
|
||||
|
||||
public static function inviteUser(
|
||||
string $email,
|
||||
string $role,
|
||||
Workspace $workspace,
|
||||
Carbon $validUntil = null
|
||||
): self {
|
||||
// Generate a token
|
||||
do {
|
||||
$token = Str::random(100);
|
||||
} while (UserInvite::where('token', $token)->exists());
|
||||
|
||||
$invite = self::create([
|
||||
'email' => $email,
|
||||
'role' => $role,
|
||||
'workspace_id' => $workspace->id,
|
||||
'valid_until' => $validUntil ?? now()->addDays(7),
|
||||
'token' => $token,
|
||||
]);
|
||||
$invite->sendEmail();
|
||||
return $invite;
|
||||
}
|
||||
|
||||
public function getLink()
|
||||
{
|
||||
return front_url('/register?email=' . urlencode($this->email) . '&invite_token=' . urlencode($this->token));
|
||||
}
|
||||
|
||||
public function hasExpired()
|
||||
{
|
||||
return Carbon::parse($this->valid_until)->isPast();
|
||||
}
|
||||
|
||||
public function workspace()
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
public function markAsAccepted()
|
||||
{
|
||||
$this->update(['status' => self::ACCEPTED_STATUS]);
|
||||
WorkspaceUsersUpdated::dispatch($this->workspace);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sendEmail()
|
||||
{
|
||||
Mail::to($this->email)->send(new UserInvitationEmail($this));
|
||||
}
|
||||
|
||||
public function scopeNotExpired($query)
|
||||
{
|
||||
return $query->where('valid_until', '>', now());
|
||||
}
|
||||
|
||||
public function scopePending($query)
|
||||
{
|
||||
return $query->where('status', self::PENDING_STATUS);
|
||||
}
|
||||
}
|
||||
28
app/Models/UserWorkspace.php
Normal file
28
app/Models/UserWorkspace.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserWorkspace extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'user_workspace';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'workspace_id',
|
||||
'role',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function workspace()
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Models\Traits\CachableAttributes;
|
||||
use App\Models\Traits\CachesAttributes;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Workspace extends Model implements CachableAttributes
|
||||
{
|
||||
@@ -50,7 +51,7 @@ class Workspace extends Model implements CachableAttributes
|
||||
|
||||
public function getMaxFileSizeAttribute()
|
||||
{
|
||||
if (is_null(config('cashier.key'))) {
|
||||
if (!pricing_enabled()) {
|
||||
return self::MAX_FILE_SIZE_PRO;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ class Workspace extends Model implements CachableAttributes
|
||||
|
||||
public function getCustomDomainCountLimitAttribute()
|
||||
{
|
||||
if (is_null(config('cashier.key'))) {
|
||||
if (!pricing_enabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ class Workspace extends Model implements CachableAttributes
|
||||
|
||||
public function getIsProAttribute()
|
||||
{
|
||||
if (is_null(config('cashier.key'))) {
|
||||
if (!pricing_enabled()) {
|
||||
return true; // If no paid plan so TRUE for ALL
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ class Workspace extends Model implements CachableAttributes
|
||||
|
||||
public function getIsTrialingAttribute()
|
||||
{
|
||||
if (is_null(config('cashier.key'))) {
|
||||
if (!pricing_enabled()) {
|
||||
return false; // If no paid plan so FALSE for ALL
|
||||
}
|
||||
|
||||
@@ -131,7 +132,7 @@ class Workspace extends Model implements CachableAttributes
|
||||
|
||||
public function getIsEnterpriseAttribute()
|
||||
{
|
||||
if (is_null(config('cashier.key'))) {
|
||||
if (!pricing_enabled()) {
|
||||
return true; // If no paid plan so TRUE for ALL
|
||||
}
|
||||
|
||||
@@ -181,11 +182,21 @@ class Workspace extends Model implements CachableAttributes
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
public function invites()
|
||||
{
|
||||
return $this->hasMany(UserInvite::class);
|
||||
}
|
||||
|
||||
public function owners()
|
||||
{
|
||||
return $this->users()->wherePivot('role', 'admin');
|
||||
}
|
||||
|
||||
public function billingOwners(): Collection
|
||||
{
|
||||
return $this->owners->filter(fn ($owner) => $owner->is_subscribed);
|
||||
}
|
||||
|
||||
public function forms()
|
||||
{
|
||||
return $this->hasMany(Form::class);
|
||||
|
||||
Reference in New Issue
Block a user