mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-18 21:22:15 +01:00
Merge branch 'main' of github.com:solidtime-io/solidtime
This commit is contained in:
@@ -12,7 +12,9 @@ use App\Models\Organization;
|
|||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
|
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
|
||||||
|
use Laravel\Jetstream\Mail\TeamInvitation;
|
||||||
|
|
||||||
class InvitationController extends Controller
|
class InvitationController extends Controller
|
||||||
{
|
{
|
||||||
@@ -64,6 +66,22 @@ class InvitationController extends Controller
|
|||||||
return response()->json(null, 204);
|
return response()->json(null, 204);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend email for a pending invitation
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
*
|
||||||
|
* @operationId resendInvitationEmail
|
||||||
|
*/
|
||||||
|
public function resend(Organization $organization, OrganizationInvitation $invitation): JsonResponse
|
||||||
|
{
|
||||||
|
$this->checkPermission($organization, 'invitations:resend', $invitation);
|
||||||
|
|
||||||
|
Mail::to($invitation->email)->send(new TeamInvitation($invitation));
|
||||||
|
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a pending invitation
|
* Remove a pending invitation
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ use Laravel\Jetstream\Team as JetstreamTeam;
|
|||||||
* @property User $owner
|
* @property User $owner
|
||||||
* @property Carbon|null $created_at
|
* @property Carbon|null $created_at
|
||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property Collection<User> $users
|
* @property Collection<int, User> $users
|
||||||
* @property Collection<string, User> $realUsers
|
* @property Collection<int, User> $realUsers
|
||||||
|
* @property-read Collection<int, OrganizationInvitation> $teamInvitations
|
||||||
* @property Membership $membership
|
* @property Membership $membership
|
||||||
*
|
*
|
||||||
* @method HasMany<OrganizationInvitation> teamInvitations()
|
* @method HasMany<OrganizationInvitation> teamInvitations()
|
||||||
@@ -83,7 +84,7 @@ class Organization extends JetstreamTeam
|
|||||||
/**
|
/**
|
||||||
* Get all the non-placeholder users of the organization including its owner.
|
* Get all the non-placeholder users of the organization including its owner.
|
||||||
*
|
*
|
||||||
* @return Collection<string, User>
|
* @return Collection<int, User>
|
||||||
*/
|
*/
|
||||||
public function allRealUsers(): Collection
|
public function allRealUsers(): Collection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
* @property int|null $billable_rate
|
* @property int|null $billable_rate
|
||||||
* @property-read Organization $organization
|
* @property-read Organization $organization
|
||||||
* @property-read Client|null $client
|
* @property-read Client|null $client
|
||||||
* @property-read Collection<Task> $tasks
|
* @property-read Collection<int, Task> $tasks
|
||||||
*
|
*
|
||||||
* @method Builder<Project> visibleByUser(User $user)
|
* @method Builder<Project> visibleByUser(User $user)
|
||||||
* @method static ProjectFactory factory()
|
* @method static ProjectFactory factory()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use Illuminate\Support\Carbon;
|
|||||||
* @property Carbon|null $updated_at
|
* @property Carbon|null $updated_at
|
||||||
* @property-read Project $project
|
* @property-read Project $project
|
||||||
* @property-read Organization $organization
|
* @property-read Organization $organization
|
||||||
* @property-read Collection<TimeEntry> $timeEntries
|
* @property-read Collection<int, TimeEntry> $timeEntries
|
||||||
*
|
*
|
||||||
* @method static TaskFactory factory()
|
* @method static TaskFactory factory()
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ use Illuminate\Session\Middleware\AuthenticateSession;
|
|||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
|
use Nwidart\Modules\Facades\Module;
|
||||||
use pxlrbt\FilamentEnvironmentIndicator\EnvironmentIndicatorPlugin;
|
use pxlrbt\FilamentEnvironmentIndicator\EnvironmentIndicatorPlugin;
|
||||||
|
|
||||||
class AdminPanelProvider extends PanelProvider
|
class AdminPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
public function panel(Panel $panel): Panel
|
public function panel(Panel $panel): Panel
|
||||||
{
|
{
|
||||||
return $panel
|
$panel->default()
|
||||||
->default()
|
|
||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->colors([
|
->colors([
|
||||||
@@ -72,5 +72,26 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
->authMiddleware([
|
->authMiddleware([
|
||||||
Authenticate::class,
|
Authenticate::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$modules = Module::allEnabled();
|
||||||
|
|
||||||
|
foreach ($modules as $module) {
|
||||||
|
$panel->discoverResources(
|
||||||
|
in: module_path($module->getName(), 'app/Filament/Resources'),
|
||||||
|
for: 'Extensions\\'.$module->getName().'\\App\\Filament\\Resources'
|
||||||
|
);
|
||||||
|
|
||||||
|
$panel->discoverPages(
|
||||||
|
in: module_path($module->getName(), 'app/Filament/Pages'),
|
||||||
|
for: 'Extensions\\'.$module->getName().'\\App\\Filament\\Pages'
|
||||||
|
);
|
||||||
|
|
||||||
|
$panel->discoverWidgets(
|
||||||
|
in: module_path($module->getName(), 'app/Filament/Widgets'),
|
||||||
|
for: 'Extensions\\'.$module->getName().'\\App\\Filament\\Widgets'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $panel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ use App\Actions\Fortify\CreateNewUser;
|
|||||||
use App\Actions\Fortify\ResetUserPassword;
|
use App\Actions\Fortify\ResetUserPassword;
|
||||||
use App\Actions\Fortify\UpdateUserPassword;
|
use App\Actions\Fortify\UpdateUserPassword;
|
||||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Cache\RateLimiting\Limit;
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@@ -35,6 +37,20 @@ class FortifyServiceProvider extends ServiceProvider
|
|||||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||||
|
|
||||||
|
Fortify::authenticateUsing(function (Request $request): ?User {
|
||||||
|
/** @var User|null $user */
|
||||||
|
$user = User::query()
|
||||||
|
->where('email', $request->email)
|
||||||
|
->where('is_placeholder', '=', false)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($user !== null && Hash::check($request->password, $user->password)) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
RateLimiter::for('login', function (Request $request) {
|
RateLimiter::for('login', function (Request $request) {
|
||||||
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use App\Enums\Role;
|
|||||||
use App\Enums\Weekday;
|
use App\Enums\Weekday;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
|
use App\Models\User;
|
||||||
use App\Service\TimezoneService;
|
use App\Service\TimezoneService;
|
||||||
use Brick\Money\Currency;
|
use Brick\Money\Currency;
|
||||||
use Brick\Money\ISOCurrencyProvider;
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
@@ -71,6 +72,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'project-members:update',
|
'project-members:update',
|
||||||
'project-members:delete',
|
'project-members:delete',
|
||||||
'tasks:view',
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
'tasks:create',
|
'tasks:create',
|
||||||
'tasks:update',
|
'tasks:update',
|
||||||
'tasks:delete',
|
'tasks:delete',
|
||||||
@@ -95,13 +97,14 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'import',
|
'import',
|
||||||
'invitations:view',
|
'invitations:view',
|
||||||
'invitations:create',
|
'invitations:create',
|
||||||
|
'invitations:resend',
|
||||||
'invitations:remove',
|
'invitations:remove',
|
||||||
'members:view',
|
'members:view',
|
||||||
'members:invite-placeholder',
|
'members:invite-placeholder',
|
||||||
'members:change-role',
|
'members:change-role',
|
||||||
'members:update',
|
'members:update',
|
||||||
'members:delete',
|
'members:delete',
|
||||||
])->description('Owner users can perform any action.');
|
])->description('Owner users can perform any action. There is only one owner per organization.');
|
||||||
|
|
||||||
Jetstream::role(Role::Admin->value, 'Administrator', [
|
Jetstream::role(Role::Admin->value, 'Administrator', [
|
||||||
'projects:view',
|
'projects:view',
|
||||||
@@ -114,6 +117,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'project-members:update',
|
'project-members:update',
|
||||||
'project-members:delete',
|
'project-members:delete',
|
||||||
'tasks:view',
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
'tasks:create',
|
'tasks:create',
|
||||||
'tasks:update',
|
'tasks:update',
|
||||||
'tasks:delete',
|
'tasks:delete',
|
||||||
@@ -136,9 +140,13 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'organizations:view',
|
'organizations:view',
|
||||||
'organizations:update',
|
'organizations:update',
|
||||||
'import',
|
'import',
|
||||||
|
'invitations:view',
|
||||||
|
'invitations:create',
|
||||||
|
'invitations:resend',
|
||||||
|
'invitations:remove',
|
||||||
'members:view',
|
'members:view',
|
||||||
'members:invite-placeholder',
|
'members:invite-placeholder',
|
||||||
])->description('Administrator users can perform any action.');
|
])->description('Administrator users can perform any action, except accessing the billing dashboard.');
|
||||||
|
|
||||||
Jetstream::role(Role::Manager->value, 'Manager', [
|
Jetstream::role(Role::Manager->value, 'Manager', [
|
||||||
'projects:view',
|
'projects:view',
|
||||||
@@ -151,6 +159,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'project-members:update',
|
'project-members:update',
|
||||||
'project-members:delete',
|
'project-members:delete',
|
||||||
'tasks:view',
|
'tasks:view',
|
||||||
|
'tasks:view:all',
|
||||||
'tasks:create',
|
'tasks:create',
|
||||||
'tasks:update',
|
'tasks:update',
|
||||||
'tasks:delete',
|
'tasks:delete',
|
||||||
@@ -171,8 +180,9 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'clients:update',
|
'clients:update',
|
||||||
'clients:delete',
|
'clients:delete',
|
||||||
'organizations:view',
|
'organizations:view',
|
||||||
|
'invitations:view',
|
||||||
'members:view',
|
'members:view',
|
||||||
])->description('Managers have the ability to read, create, and update their own time entries as well as those of their team.');
|
])->description('Managers have full access to all projects, time entries, ect. but cannot manage the organization (add/remove member, edit the organization, ect.).');
|
||||||
|
|
||||||
Jetstream::role(Role::Employee->value, 'Employee', [
|
Jetstream::role(Role::Employee->value, 'Employee', [
|
||||||
'projects:view',
|
'projects:view',
|
||||||
@@ -183,7 +193,7 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'time-entries:update:own',
|
'time-entries:update:own',
|
||||||
'time-entries:delete:own',
|
'time-entries:delete:own',
|
||||||
'organizations:view',
|
'organizations:view',
|
||||||
])->description('Employees have the ability to read, create, and update their own time entries.');
|
])->description('Employees have the ability to read, create, and update their own time entries and they can see the projects that they are members of.');
|
||||||
|
|
||||||
Jetstream::role(Role::Placeholder->value, 'Placeholder', [
|
Jetstream::role(Role::Placeholder->value, 'Placeholder', [
|
||||||
])->description('Placeholders are used for importing data. They cannot log in and have no permissions.');
|
])->description('Placeholders are used for importing data. They cannot log in and have no permissions.');
|
||||||
@@ -201,7 +211,41 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
->whenRendering(
|
->whenRendering(
|
||||||
'Teams/Show',
|
'Teams/Show',
|
||||||
function (Request $request, array $data): array {
|
function (Request $request, array $data): array {
|
||||||
|
/** @var Organization $teamModel */
|
||||||
|
$teamModel = $data['team'];
|
||||||
|
$owner = $teamModel->owner;
|
||||||
|
|
||||||
return array_merge($data, [
|
return array_merge($data, [
|
||||||
|
'team' => [
|
||||||
|
'id' => $teamModel->getKey(),
|
||||||
|
'name' => $teamModel->name,
|
||||||
|
'currency' => $teamModel->currency,
|
||||||
|
'owner' => [
|
||||||
|
'id' => $owner->getKey(),
|
||||||
|
'name' => $owner->name,
|
||||||
|
'email' => $owner->email,
|
||||||
|
'profile_photo_url' => $owner->profile_photo_url,
|
||||||
|
],
|
||||||
|
'users' => $teamModel->users->map(function (User $user): array {
|
||||||
|
return [
|
||||||
|
'id' => $user->getKey(),
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
'profile_photo_url' => $user->profile_photo_url,
|
||||||
|
'membership' => [
|
||||||
|
'id' => $user->membership->id,
|
||||||
|
'role' => $user->membership->role,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
'team_invitations' => $teamModel->teamInvitations->map(function (OrganizationInvitation $invitation): array {
|
||||||
|
return [
|
||||||
|
'id' => $invitation->getKey(),
|
||||||
|
'email' => $invitation->email,
|
||||||
|
'role' => $invitation->role,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
],
|
||||||
'currencies' => array_map(function (Currency $currency): string {
|
'currencies' => array_map(function (Currency $currency): string {
|
||||||
return $currency->getName();
|
return $currency->getName();
|
||||||
}, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()),
|
}, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
|||||||
|
|
||||||
$this->hideSensitiveRequestDetails();
|
$this->hideSensitiveRequestDetails();
|
||||||
|
|
||||||
Telescope::filter(function (IncomingEntry $entry) {
|
Telescope::filter(function (IncomingEntry $entry): bool {
|
||||||
if ($this->app->environment('local')) {
|
if ($this->app->environment('local')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,12 +162,12 @@ class ClockifyTimeEntriesImporter extends DefaultImporter
|
|||||||
#[\Override]
|
#[\Override]
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
return __('importer.toggl_data_importer.name');
|
return __('importer.clockify_time_entries.name');
|
||||||
}
|
}
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
public function getDescription(): string
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
return __('importer.toggl_data_importer.description');
|
return __('importer.clockify_time_entries.description');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ return [
|
|||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'stack_production' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => ['single', 'sentry'],
|
||||||
|
],
|
||||||
|
|
||||||
'daily' => [
|
'daily' => [
|
||||||
'driver' => 'daily',
|
'driver' => 'daily',
|
||||||
'path' => storage_path('logs/laravel.log'),
|
'path' => storage_path('logs/laravel.log'),
|
||||||
@@ -84,6 +89,12 @@ return [
|
|||||||
'replace_placeholders' => true,
|
'replace_placeholders' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'sentry' => [
|
||||||
|
'driver' => 'sentry',
|
||||||
|
'level' => env('LOG_LEVEL', 'error'),
|
||||||
|
'bubble' => true,
|
||||||
|
],
|
||||||
|
|
||||||
'papertrail' => [
|
'papertrail' => [
|
||||||
'driver' => 'monolog',
|
'driver' => 'monolog',
|
||||||
'level' => env('LOG_LEVEL', 'debug'),
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<msapplication>
|
<msapplication>
|
||||||
<tile>
|
<tile>
|
||||||
<square150x150logo src="/favicons/mstile-150x150.png"/>
|
<square150x150logo src="/favicons/mstile-150x150.png"/>
|
||||||
<TileColor>#da532c</TileColor>
|
<TileColor>#000000</TileColor>
|
||||||
</tile>
|
</tile>
|
||||||
</msapplication>
|
</msapplication>
|
||||||
</browserconfig>
|
</browserconfig>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#ffffff",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#000000",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,16 +90,14 @@ const switchToTeam = (team: Organization) => {
|
|||||||
|
|
||||||
<DropdownLink
|
<DropdownLink
|
||||||
v-if="isBillingActivated()"
|
v-if="isBillingActivated()"
|
||||||
href="
|
href="/billing">
|
||||||
/billing
|
|
||||||
">
|
|
||||||
Billing
|
Billing
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
|
|
||||||
<DropdownLink
|
<DropdownLink
|
||||||
v-if="page.props.jetstream.canCreateTeams"
|
v-if="page.props.jetstream.canCreateTeams"
|
||||||
:href="route('teams.create')">
|
:href="route('teams.create')">
|
||||||
Create New Team
|
Create new organization
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
|
|
||||||
<!-- Organization Switcher -->
|
<!-- Organization Switcher -->
|
||||||
@@ -107,7 +105,7 @@ const switchToTeam = (team: Organization) => {
|
|||||||
<div class="border-t border-card-background-separator" />
|
<div class="border-t border-card-background-separator" />
|
||||||
|
|
||||||
<div class="block px-4 py-2 text-xs text-muted">
|
<div class="block px-4 py-2 text-xs text-muted">
|
||||||
Switch Teams
|
Switch Organizations
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const page = usePage<{
|
|||||||
<template #title> Organization Details</template>
|
<template #title> Organization Details</template>
|
||||||
|
|
||||||
<template #description>
|
<template #description>
|
||||||
Create a new team to collaborate with others on projects.
|
Create a new organization to collaborate with others on projects.
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #form>
|
<template #form>
|
||||||
|
|||||||
@@ -26,21 +26,21 @@ const deleteTeam = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ActionSection>
|
<ActionSection>
|
||||||
<template #title> Delete Team </template>
|
<template #title> Delete Organization </template>
|
||||||
|
|
||||||
<template #description> Permanently delete this team. </template>
|
<template #description> Permanently delete this organization. </template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="max-w-xl text-sm text-muted">
|
<div class="max-w-xl text-sm text-muted">
|
||||||
Once a team is deleted, all of its resources and data will be
|
Once a organization is deleted, all of its resources and data will be
|
||||||
permanently deleted. Before deleting this team, please download
|
permanently deleted. Before deleting this organization, please download
|
||||||
any data or information regarding this team that you wish to
|
any data or information regarding this organization that you wish to
|
||||||
retain.
|
retain.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
<DangerButton @click="confirmTeamDeletion">
|
<DangerButton @click="confirmTeamDeletion">
|
||||||
Delete Team
|
Delete Organization
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const deleteTeam = () => {
|
|||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
:show="confirmingTeamDeletion"
|
:show="confirmingTeamDeletion"
|
||||||
@close="confirmingTeamDeletion = false">
|
@close="confirmingTeamDeletion = false">
|
||||||
<template #title> Delete Team </template>
|
<template #title> Delete Organization </template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
Are you sure you want to delete this team? Once a team is
|
Are you sure you want to delete this team? Once a team is
|
||||||
@@ -66,7 +66,7 @@ const deleteTeam = () => {
|
|||||||
:class="{ 'opacity-25': form.processing }"
|
:class="{ 'opacity-25': form.processing }"
|
||||||
:disabled="form.processing"
|
:disabled="form.processing"
|
||||||
@click="deleteTeam">
|
@click="deleteTeam">
|
||||||
Delete Team
|
Delete Organization
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</template>
|
</template>
|
||||||
</ConfirmationModal>
|
</ConfirmationModal>
|
||||||
|
|||||||
@@ -130,10 +130,10 @@ const displayableRole = (role: string) => {
|
|||||||
|
|
||||||
<!-- Add Organization Member -->
|
<!-- Add Organization Member -->
|
||||||
<FormSection @submitted="addTeamMember">
|
<FormSection @submitted="addTeamMember">
|
||||||
<template #title> Add Team Member</template>
|
<template #title> Add Organization Member</template>
|
||||||
|
|
||||||
<template #description>
|
<template #description>
|
||||||
Add a new team member to your team, allowing them to
|
Add a new member to your organization, allowing them to
|
||||||
collaborate with you.
|
collaborate with you.
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ const displayableRole = (role: string) => {
|
|||||||
<div class="col-span-6">
|
<div class="col-span-6">
|
||||||
<div class="max-w-xl text-sm text-muted">
|
<div class="max-w-xl text-sm text-muted">
|
||||||
Please provide the email address of the person you
|
Please provide the email address of the person you
|
||||||
would like to add to this team.
|
would like to add to this organization.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -257,8 +257,8 @@ const displayableRole = (role: string) => {
|
|||||||
<template #title> Pending Team Invitations</template>
|
<template #title> Pending Team Invitations</template>
|
||||||
|
|
||||||
<template #description>
|
<template #description>
|
||||||
These people have been invited to your team and have been
|
These people have been invited to your organization and have been
|
||||||
sent an invitation email. They may join the team by
|
sent an invitation email. They may join the organization by
|
||||||
accepting the email invitation.
|
accepting the email invitation.
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -293,10 +293,10 @@ const displayableRole = (role: string) => {
|
|||||||
|
|
||||||
<!-- Manage Organization Members -->
|
<!-- Manage Organization Members -->
|
||||||
<ActionSection class="mt-10 sm:mt-0">
|
<ActionSection class="mt-10 sm:mt-0">
|
||||||
<template #title> Team Members</template>
|
<template #title> Organization Members</template>
|
||||||
|
|
||||||
<template #description>
|
<template #description>
|
||||||
All of the people that are part of this team.
|
All of the people that are part of this organization.
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Organization Member List -->
|
<!-- Organization Member List -->
|
||||||
@@ -443,10 +443,10 @@ const displayableRole = (role: string) => {
|
|||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
:show="confirmingLeavingTeam"
|
:show="confirmingLeavingTeam"
|
||||||
@close="confirmingLeavingTeam = false">
|
@close="confirmingLeavingTeam = false">
|
||||||
<template #title> Leave Team</template>
|
<template #title> Leave Organization</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
Are you sure you would like to leave this team?
|
Are you sure you would like to leave this organization?
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -471,7 +471,7 @@ const displayableRole = (role: string) => {
|
|||||||
<template #title> Remove Team Member</template>
|
<template #title> Remove Team Member</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
Are you sure you would like to remove this person from the team?
|
Are you sure you would like to remove this person from the organization?
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ const updateTeamName = () => {
|
|||||||
<template #title> Organization Name</template>
|
<template #title> Organization Name</template>
|
||||||
|
|
||||||
<template #description>
|
<template #description>
|
||||||
The team's name and owner information.
|
The organization's name and owner information.
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #form>
|
<template #form>
|
||||||
<!-- Organization Owner Information -->
|
<!-- Organization Owner Information -->
|
||||||
<div class="col-span-6 flex items-center justify-between">
|
<div class="col-span-6 flex items-center justify-between">
|
||||||
<div class="">
|
<div class="">
|
||||||
<InputLabel value="Team Owner" />
|
<InputLabel value="Organization Owner" />
|
||||||
|
|
||||||
<div class="flex items-center mt-2">
|
<div class="flex items-center mt-2">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<link rel="manifest" href="/favicons/site.webmanifest">
|
<link rel="manifest" href="/favicons/site.webmanifest">
|
||||||
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#000000">
|
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#000000">
|
||||||
<link rel="shortcut icon" href="/favicons/favicon.ico">
|
<link rel="shortcut icon" href="/favicons/favicon.ico">
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#000000">
|
||||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml">
|
<meta name="msapplication-config" content="/favicons/browserconfig.xml">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#000000">
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
@routes
|
@routes
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Route::middleware([
|
|||||||
Route::name('invitations.')->group(static function () {
|
Route::name('invitations.')->group(static function () {
|
||||||
Route::get('/organizations/{organization}/invitations', [InvitationController::class, 'index'])->name('index');
|
Route::get('/organizations/{organization}/invitations', [InvitationController::class, 'index'])->name('index');
|
||||||
Route::post('/organizations/{organization}/invitations', [InvitationController::class, 'store'])->name('store');
|
Route::post('/organizations/{organization}/invitations', [InvitationController::class, 'store'])->name('store');
|
||||||
|
Route::post('/organizations/{organization}/invitations/{invitation}/resend', [InvitationController::class, 'resend'])->name('resend');
|
||||||
Route::delete('/organizations/{organization}/invitations/{invitation}', [InvitationController::class, 'destroy'])->name('destroy');
|
Route::delete('/organizations/{organization}/invitations/{invitation}', [InvitationController::class, 'destroy'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace Tests;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
use TiMacDonald\Log\LogFake;
|
use TiMacDonald\Log\LogFake;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
abstract class TestCase extends BaseTestCase
|
||||||
@@ -15,6 +16,7 @@ abstract class TestCase extends BaseTestCase
|
|||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
Mail::fake();
|
||||||
LogFake::bind();
|
LogFake::bind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Unit\Endpoint\Api\V1;
|
namespace Tests\Unit\Endpoint\Api\V1;
|
||||||
|
|
||||||
use App\Models\OrganizationInvitation;
|
use App\Models\OrganizationInvitation;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Laravel\Jetstream\Mail\TeamInvitation;
|
||||||
use Laravel\Passport\Passport;
|
use Laravel\Passport\Passport;
|
||||||
|
|
||||||
class InvitationEndpointTest extends ApiEndpointTestAbstract
|
class InvitationEndpointTest extends ApiEndpointTestAbstract
|
||||||
@@ -77,6 +79,65 @@ class InvitationEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$this->assertEquals('employee', $invitation->role);
|
$this->assertEquals('employee', $invitation->role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_resend_fails_if_user_has_no_permission_to_resend_the_invitation(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
]);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
$invitation = OrganizationInvitation::factory()->forOrganization($data->organization)->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.invitations.resend', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
$invitation->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Mail::assertNothingSent();
|
||||||
|
Mail::assertNothingQueued();
|
||||||
|
$response->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_resend_fails_if_invitation_belongs_to_different_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'invitations:resend',
|
||||||
|
]);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
$invitation = OrganizationInvitation::factory()->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.invitations.resend', [$data->organization->getKey(), $invitation->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Mail::assertNothingSent();
|
||||||
|
Mail::assertNothingQueued();
|
||||||
|
$response->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_resend_resends_invitation_email(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'invitations:resend',
|
||||||
|
]);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
$invitation = OrganizationInvitation::factory()->forOrganization($data->organization)->create();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.invitations.resend', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
$invitation->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Mail::assertSent(fn (TeamInvitation $mail): bool => $mail->invitation->is($invitation));
|
||||||
|
Mail::assertNothingQueued();
|
||||||
|
$response->assertStatus(204);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_delete_fails_if_user_has_no_permission_to_remove_invitations(): void
|
public function test_delete_fails_if_user_has_no_permission_to_remove_invitations(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
Reference in New Issue
Block a user