diff --git a/app/Http/Controllers/Api/V1/InvitationController.php b/app/Http/Controllers/Api/V1/InvitationController.php index 397decf0..94875cbc 100644 --- a/app/Http/Controllers/Api/V1/InvitationController.php +++ b/app/Http/Controllers/Api/V1/InvitationController.php @@ -12,7 +12,9 @@ use App\Models\Organization; use App\Models\OrganizationInvitation; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\Mail; use Laravel\Jetstream\Contracts\InvitesTeamMembers; +use Laravel\Jetstream\Mail\TeamInvitation; class InvitationController extends Controller { @@ -64,6 +66,22 @@ class InvitationController extends Controller 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 * diff --git a/app/Models/Organization.php b/app/Models/Organization.php index 2297a0f0..26d1e270 100644 --- a/app/Models/Organization.php +++ b/app/Models/Organization.php @@ -27,8 +27,9 @@ use Laravel\Jetstream\Team as JetstreamTeam; * @property User $owner * @property Carbon|null $created_at * @property Carbon|null $updated_at - * @property Collection $users - * @property Collection $realUsers + * @property Collection $users + * @property Collection $realUsers + * @property-read Collection $teamInvitations * @property Membership $membership * * @method HasMany teamInvitations() @@ -83,7 +84,7 @@ class Organization extends JetstreamTeam /** * Get all the non-placeholder users of the organization including its owner. * - * @return Collection + * @return Collection */ public function allRealUsers(): Collection { diff --git a/app/Models/Project.php b/app/Models/Project.php index 1713fa02..48a78002 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -22,7 +22,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property int|null $billable_rate * @property-read Organization $organization * @property-read Client|null $client - * @property-read Collection $tasks + * @property-read Collection $tasks * * @method Builder visibleByUser(User $user) * @method static ProjectFactory factory() diff --git a/app/Models/Task.php b/app/Models/Task.php index 7e331e05..e868c93b 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -22,7 +22,7 @@ use Illuminate\Support\Carbon; * @property Carbon|null $updated_at * @property-read Project $project * @property-read Organization $organization - * @property-read Collection $timeEntries + * @property-read Collection $timeEntries * * @method static TaskFactory factory() */ diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 2fd6d226..d6b25610 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -21,14 +21,14 @@ use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Session\Middleware\StartSession; use Illuminate\Support\Facades\App; use Illuminate\View\Middleware\ShareErrorsFromSession; +use Nwidart\Modules\Facades\Module; use pxlrbt\FilamentEnvironmentIndicator\EnvironmentIndicatorPlugin; class AdminPanelProvider extends PanelProvider { public function panel(Panel $panel): Panel { - return $panel - ->default() + $panel->default() ->id('admin') ->path('admin') ->colors([ @@ -72,5 +72,26 @@ class AdminPanelProvider extends PanelProvider ->authMiddleware([ 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; } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 7a533eb3..bd5166d4 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -8,8 +8,10 @@ use App\Actions\Fortify\CreateNewUser; use App\Actions\Fortify\ResetUserPassword; use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; +use App\Models\User; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; @@ -35,6 +37,20 @@ class FortifyServiceProvider extends ServiceProvider Fortify::updateUserPasswordsUsing(UpdateUserPassword::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) { $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); diff --git a/app/Providers/JetstreamServiceProvider.php b/app/Providers/JetstreamServiceProvider.php index bc787977..6f192680 100644 --- a/app/Providers/JetstreamServiceProvider.php +++ b/app/Providers/JetstreamServiceProvider.php @@ -16,6 +16,7 @@ use App\Enums\Role; use App\Enums\Weekday; use App\Models\Organization; use App\Models\OrganizationInvitation; +use App\Models\User; use App\Service\TimezoneService; use Brick\Money\Currency; use Brick\Money\ISOCurrencyProvider; @@ -71,6 +72,7 @@ class JetstreamServiceProvider extends ServiceProvider 'project-members:update', 'project-members:delete', 'tasks:view', + 'tasks:view:all', 'tasks:create', 'tasks:update', 'tasks:delete', @@ -95,13 +97,14 @@ class JetstreamServiceProvider extends ServiceProvider 'import', 'invitations:view', 'invitations:create', + 'invitations:resend', 'invitations:remove', 'members:view', 'members:invite-placeholder', 'members:change-role', 'members:update', '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', [ 'projects:view', @@ -114,6 +117,7 @@ class JetstreamServiceProvider extends ServiceProvider 'project-members:update', 'project-members:delete', 'tasks:view', + 'tasks:view:all', 'tasks:create', 'tasks:update', 'tasks:delete', @@ -136,9 +140,13 @@ class JetstreamServiceProvider extends ServiceProvider 'organizations:view', 'organizations:update', 'import', + 'invitations:view', + 'invitations:create', + 'invitations:resend', + 'invitations:remove', 'members:view', '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', [ 'projects:view', @@ -151,6 +159,7 @@ class JetstreamServiceProvider extends ServiceProvider 'project-members:update', 'project-members:delete', 'tasks:view', + 'tasks:view:all', 'tasks:create', 'tasks:update', 'tasks:delete', @@ -171,8 +180,9 @@ class JetstreamServiceProvider extends ServiceProvider 'clients:update', 'clients:delete', 'organizations:view', + 'invitations: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', [ 'projects:view', @@ -183,7 +193,7 @@ class JetstreamServiceProvider extends ServiceProvider 'time-entries:update:own', 'time-entries:delete:own', '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', [ ])->description('Placeholders are used for importing data. They cannot log in and have no permissions.'); @@ -201,7 +211,41 @@ class JetstreamServiceProvider extends ServiceProvider ->whenRendering( 'Teams/Show', function (Request $request, array $data): array { + /** @var Organization $teamModel */ + $teamModel = $data['team']; + $owner = $teamModel->owner; + 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 { return $currency->getName(); }, ISOCurrencyProvider::getInstance()->getAvailableCurrencies()), diff --git a/app/Providers/TelescopeServiceProvider.php b/app/Providers/TelescopeServiceProvider.php index ded37e5c..62817a52 100644 --- a/app/Providers/TelescopeServiceProvider.php +++ b/app/Providers/TelescopeServiceProvider.php @@ -21,7 +21,7 @@ class TelescopeServiceProvider extends TelescopeApplicationServiceProvider $this->hideSensitiveRequestDetails(); - Telescope::filter(function (IncomingEntry $entry) { + Telescope::filter(function (IncomingEntry $entry): bool { if ($this->app->environment('local')) { return true; } diff --git a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php index f5ad09dd..a7e52ee5 100644 --- a/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php +++ b/app/Service/Import/Importers/ClockifyTimeEntriesImporter.php @@ -162,12 +162,12 @@ class ClockifyTimeEntriesImporter extends DefaultImporter #[\Override] public function getName(): string { - return __('importer.toggl_data_importer.name'); + return __('importer.clockify_time_entries.name'); } #[\Override] public function getDescription(): string { - return __('importer.toggl_data_importer.description'); + return __('importer.clockify_time_entries.description'); } } diff --git a/config/logging.php b/config/logging.php index cd3851a0..7599b746 100644 --- a/config/logging.php +++ b/config/logging.php @@ -67,6 +67,11 @@ return [ 'replace_placeholders' => true, ], + 'stack_production' => [ + 'driver' => 'stack', + 'channels' => ['single', 'sentry'], + ], + 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), @@ -84,6 +89,12 @@ return [ 'replace_placeholders' => true, ], + 'sentry' => [ + 'driver' => 'sentry', + 'level' => env('LOG_LEVEL', 'error'), + 'bubble' => true, + ], + 'papertrail' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), diff --git a/public/favicons/browserconfig.xml b/public/favicons/browserconfig.xml index d469e489..9824d87b 100644 --- a/public/favicons/browserconfig.xml +++ b/public/favicons/browserconfig.xml @@ -3,7 +3,7 @@ - #da532c + #000000 diff --git a/public/favicons/site.webmanifest b/public/favicons/site.webmanifest index 3035a89b..c620e597 100644 --- a/public/favicons/site.webmanifest +++ b/public/favicons/site.webmanifest @@ -13,7 +13,7 @@ "type": "image/png" } ], - "theme_color": "#ffffff", - "background_color": "#ffffff", + "theme_color": "#000000", + "background_color": "#000000", "display": "standalone" } diff --git a/resources/js/Components/OrganizationSwitcher.vue b/resources/js/Components/OrganizationSwitcher.vue index db43be99..05e916da 100644 --- a/resources/js/Components/OrganizationSwitcher.vue +++ b/resources/js/Components/OrganizationSwitcher.vue @@ -90,16 +90,14 @@ const switchToTeam = (team: Organization) => { + href="/billing"> Billing - Create New Team + Create new organization @@ -107,7 +105,7 @@ const switchToTeam = (team: Organization) => {
- Switch Teams + Switch Organizations