Andrew Wallo 2 年前
父节点
当前提交
c6ff9cf2b0
共有 97 个文件被更改,包括 9261 次插入538 次删除
  1. 4
    1
      .env.example
  2. 85
    0
      app/Actions/FilamentCompanies/AddCompanyEmployee.php
  3. 40
    0
      app/Actions/FilamentCompanies/CreateCompany.php
  4. 32
    0
      app/Actions/FilamentCompanies/CreateConnectedAccount.php
  5. 74
    0
      app/Actions/FilamentCompanies/CreateUserFromProvider.php
  6. 17
    0
      app/Actions/FilamentCompanies/DeleteCompany.php
  7. 51
    0
      app/Actions/FilamentCompanies/DeleteUser.php
  8. 22
    0
      app/Actions/FilamentCompanies/HandleInvalidState.php
  9. 91
    0
      app/Actions/FilamentCompanies/InviteCompanyEmployee.php
  10. 55
    0
      app/Actions/FilamentCompanies/RemoveCompanyEmployee.php
  11. 25
    0
      app/Actions/FilamentCompanies/ResolveSocialiteUser.php
  12. 26
    0
      app/Actions/FilamentCompanies/SetUserPassword.php
  13. 33
    0
      app/Actions/FilamentCompanies/UpdateCompanyName.php
  14. 37
    0
      app/Actions/FilamentCompanies/UpdateConnectedAccount.php
  15. 53
    0
      app/Actions/Fortify/CreateNewUser.php
  16. 18
    0
      app/Actions/Fortify/PasswordValidationRules.php
  17. 29
    0
      app/Actions/Fortify/ResetUserPassword.php
  18. 32
    0
      app/Actions/Fortify/UpdateUserPassword.php
  19. 64
    0
      app/Actions/Fortify/UpdateUserProfileInformation.php
  20. 1
    19
      app/Exceptions/Handler.php
  21. 30
    0
      app/Filament/Pages/Companies.php
  22. 10
    0
      app/Filament/Pages/Dashboard.php
  23. 30
    0
      app/Filament/Pages/Users.php
  24. 72
    0
      app/Filament/Pages/Widgets/Companies.php
  25. 44
    0
      app/Filament/Pages/Widgets/Users.php
  26. 1
    1
      app/Http/Kernel.php
  27. 44
    0
      app/Models/Company.php
  28. 28
    0
      app/Models/CompanyInvitation.php
  29. 42
    0
      app/Models/ConnectedAccount.php
  30. 15
    0
      app/Models/Employeeship.php
  31. 38
    6
      app/Models/User.php
  32. 76
    0
      app/Policies/CompanyPolicy.php
  33. 52
    0
      app/Policies/ConnectedAccountPolicy.php
  34. 4
    1
      app/Providers/AppServiceProvider.php
  35. 1
    1
      app/Providers/AuthServiceProvider.php
  36. 132
    0
      app/Providers/FilamentCompaniesServiceProvider.php
  37. 45
    0
      app/Providers/FortifyServiceProvider.php
  38. 5
    13
      app/Providers/RouteServiceProvider.php
  39. 7
    3
      composer.json
  40. 2536
    440
      composer.lock
  41. 6
    31
      config/app.php
  42. 1
    1
      config/auth.php
  43. 122
    0
      config/filament-companies.php
  44. 334
    0
      config/filament.php
  45. 68
    0
      config/forms.php
  46. 147
    0
      config/fortify.php
  47. 9
    0
      config/logging.php
  48. 51
    0
      config/notifications.php
  49. 16
    0
      config/queue.php
  50. 2
    2
      config/sanctum.php
  51. 6
    0
      config/services.php
  52. 1
    1
      config/session.php
  53. 81
    0
      config/tables.php
  54. 31
    0
      database/factories/CompanyFactory.php
  55. 39
    8
      database/factories/UserFactory.php
  56. 7
    1
      database/migrations/2014_10_12_000000_create_users_table.php
  57. 46
    0
      database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php
  58. 30
    0
      database/migrations/2020_05_21_100000_create_companies_table.php
  59. 32
    0
      database/migrations/2020_05_21_200000_create_company_user_table.php
  60. 32
    0
      database/migrations/2020_05_21_300000_create_company_invitations_table.php
  61. 42
    0
      database/migrations/2020_12_22_000000_create_connected_accounts_table.php
  62. 31
    0
      database/migrations/2023_05_01_034040_create_sessions_table.php
  63. 3085
    0
      package-lock.json
  64. 9
    0
      package.json
  65. 2
    2
      phpunit.xml
  66. 6
    0
      postcss.config.js
  67. 5
    0
      resources/css/app.css
  68. 1
    0
      resources/css/filament.css
  69. 14
    1
      resources/js/app.js
  70. 3
    0
      resources/markdown/policy.md
  71. 3
    0
      resources/markdown/terms.md
  72. 18
    0
      resources/views/filament/components/companies/avatar-column.blade.php
  73. 18
    0
      resources/views/filament/components/users/avatar-column.blade.php
  74. 3
    0
      resources/views/filament/pages/companies.blade.php
  75. 3
    0
      resources/views/filament/pages/users.blade.php
  76. 24
    0
      resources/views/layouts/app.blade.php
  77. 1
    1
      resources/views/welcome.blade.php
  78. 8
    0
      routes/web.php
  79. 25
    0
      tailwind.config.js
  80. 44
    0
      tests/Feature/AuthenticationTest.php
  81. 24
    0
      tests/Feature/BrowserSessionsTest.php
  82. 26
    0
      tests/Feature/CreateCompanyTest.php
  83. 50
    0
      tests/Feature/DeleteAccountTest.php
  84. 45
    0
      tests/Feature/DeleteCompanyTest.php
  85. 78
    0
      tests/Feature/EmailVerificationTest.php
  86. 67
    0
      tests/Feature/InviteCompanyEmployeeTest.php
  87. 41
    0
      tests/Feature/LeaveCompanyTest.php
  88. 44
    0
      tests/Feature/PasswordConfirmationTest.php
  89. 102
    0
      tests/Feature/PasswordResetTest.php
  90. 36
    0
      tests/Feature/ProfileInformationTest.php
  91. 59
    0
      tests/Feature/RegistrationTest.php
  92. 45
    0
      tests/Feature/RemoveCompanyEmployeeTest.php
  93. 82
    0
      tests/Feature/TwoFactorAuthenticationSettingsTest.php
  94. 53
    0
      tests/Feature/UpdateCompanyEmployeeRoleTest.php
  95. 26
    0
      tests/Feature/UpdateCompanyNameTest.php
  96. 62
    0
      tests/Feature/UpdatePasswordTest.php
  97. 14
    5
      vite.config.js

+ 4
- 1
.env.example 查看文件

@@ -19,7 +19,7 @@ BROADCAST_DRIVER=log
19 19
 CACHE_DRIVER=file
20 20
 FILESYSTEM_DISK=local
21 21
 QUEUE_CONNECTION=sync
22
-SESSION_DRIVER=file
22
+SESSION_DRIVER=database
23 23
 SESSION_LIFETIME=120
24 24
 
25 25
 MEMCACHED_HOST=127.0.0.1
@@ -56,3 +56,6 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
56 56
 VITE_PUSHER_PORT="${PUSHER_PORT}"
57 57
 VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
58 58
 VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
59
+
60
+GITHUB_CLIENT_ID=
61
+GITHUB_CLIENT_SECRET=

+ 85
- 0
app/Actions/FilamentCompanies/AddCompanyEmployee.php 查看文件

@@ -0,0 +1,85 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Closure;
8
+use Illuminate\Auth\Access\AuthorizationException;
9
+use Illuminate\Contracts\Validation\Rule;
10
+use Illuminate\Support\Facades\Gate;
11
+use Illuminate\Support\Facades\Validator;
12
+use Wallo\FilamentCompanies\Contracts\AddsCompanyEmployees;
13
+use Wallo\FilamentCompanies\Events\AddingCompanyEmployee;
14
+use Wallo\FilamentCompanies\Events\CompanyEmployeeAdded;
15
+use Wallo\FilamentCompanies\FilamentCompanies;
16
+use Wallo\FilamentCompanies\Rules\Role;
17
+
18
+class AddCompanyEmployee implements AddsCompanyEmployees
19
+{
20
+    /**
21
+     * Add a new company employee to the given company.
22
+     *
23
+     * @throws AuthorizationException
24
+     */
25
+    public function add(User $user, Company $company, string $email, string|null $role = null): void
26
+    {
27
+        Gate::forUser($user)->authorize('addCompanyEmployee', $company);
28
+
29
+        $this->validate($company, $email, $role);
30
+
31
+        $newCompanyEmployee = FilamentCompanies::findUserByEmailOrFail($email);
32
+
33
+        AddingCompanyEmployee::dispatch($company, $newCompanyEmployee);
34
+
35
+        $company->users()->attach(
36
+            $newCompanyEmployee, ['role' => $role]
37
+        );
38
+
39
+        CompanyEmployeeAdded::dispatch($company, $newCompanyEmployee);
40
+    }
41
+
42
+    /**
43
+     * Validate the add employee operation.
44
+     */
45
+    protected function validate(Company $company, string $email, ?string $role): void
46
+    {
47
+        Validator::make([
48
+            'email' => $email,
49
+            'role' => $role,
50
+        ], $this->rules(), [
51
+            'email.exists' => __('filament-companies::default.errors.email_not_found'),
52
+        ])->after(
53
+            $this->ensureUserIsNotAlreadyOnCompany($company, $email)
54
+        )->validateWithBag('addCompanyEmployee');
55
+    }
56
+
57
+    /**
58
+     * Get the validation rules for adding a company employee.
59
+     *
60
+     * @return array<string, Rule|array|string>
61
+     */
62
+    protected function rules(): array
63
+    {
64
+        return array_filter([
65
+            'email' => ['required', 'email', 'exists:users'],
66
+            'role' => FilamentCompanies::hasRoles()
67
+                            ? ['required', 'string', new Role]
68
+                            : null,
69
+        ]);
70
+    }
71
+
72
+    /**
73
+     * Ensure that the user is not already on the company.
74
+     */
75
+    protected function ensureUserIsNotAlreadyOnCompany(Company $company, string $email): Closure
76
+    {
77
+        return static function ($validator) use ($company, $email) {
78
+            $validator->errors()->addIf(
79
+                $company->hasUserWithEmail($email),
80
+                'email',
81
+                __('filament-companies::default.errors.user_belongs_to_company')
82
+            );
83
+        };
84
+    }
85
+}

+ 40
- 0
app/Actions/FilamentCompanies/CreateCompany.php 查看文件

@@ -0,0 +1,40 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Auth\Access\AuthorizationException;
8
+use Illuminate\Support\Facades\Gate;
9
+use Illuminate\Support\Facades\Validator;
10
+use Wallo\FilamentCompanies\Contracts\CreatesCompanies;
11
+use Wallo\FilamentCompanies\Events\AddingCompany;
12
+use Wallo\FilamentCompanies\FilamentCompanies;
13
+
14
+class CreateCompany implements CreatesCompanies
15
+{
16
+    /**
17
+     * Validate and create a new company for the given user.
18
+     *
19
+     * @param  array<string, string>  $input
20
+     *
21
+     * @throws AuthorizationException
22
+     */
23
+    public function create(User $user, array $input): Company
24
+    {
25
+        Gate::forUser($user)->authorize('create', FilamentCompanies::newCompanyModel());
26
+
27
+        Validator::make($input, [
28
+            'name' => ['required', 'string', 'max:255'],
29
+        ])->validateWithBag('createCompany');
30
+
31
+        AddingCompany::dispatch($user);
32
+
33
+        $user->switchCompany($company = $user->ownedCompanies()->create([
34
+            'name' => $input['name'],
35
+            'personal_company' => false,
36
+        ]));
37
+
38
+        return $company;
39
+    }
40
+}

+ 32
- 0
app/Actions/FilamentCompanies/CreateConnectedAccount.php 查看文件

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use Illuminate\Contracts\Auth\Authenticatable;
6
+use Laravel\Socialite\Contracts\User as ProviderUser;
7
+use Wallo\FilamentCompanies\ConnectedAccount;
8
+use Wallo\FilamentCompanies\Contracts\CreatesConnectedAccounts;
9
+use Wallo\FilamentCompanies\Socialite;
10
+
11
+class CreateConnectedAccount implements CreatesConnectedAccounts
12
+{
13
+    /**
14
+     * Create a connected account for a given user.
15
+     */
16
+    public function create(Authenticatable $user, string $provider, ProviderUser $providerUser): ConnectedAccount
17
+    {
18
+        return Socialite::connectedAccountModel()::forceCreate([
19
+            'user_id' => $user->getAuthIdentifier(),
20
+            'provider' => strtolower($provider),
21
+            'provider_id' => $providerUser->getId(),
22
+            'name' => $providerUser->getName(),
23
+            'nickname' => $providerUser->getNickname(),
24
+            'email' => $providerUser->getEmail(),
25
+            'avatar_path' => $providerUser->getAvatar(),
26
+            'token' => $providerUser->token,
27
+            'secret' => $providerUser->tokenSecret ?? null,
28
+            'refresh_token' => $providerUser->refreshToken ?? null,
29
+            'expires_at' => property_exists($providerUser, 'expiresIn') ? now()->addSeconds($providerUser->expiresIn) : null,
30
+        ]);
31
+    }
32
+}

+ 74
- 0
app/Actions/FilamentCompanies/CreateUserFromProvider.php 查看文件

@@ -0,0 +1,74 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Support\Facades\DB;
8
+use Laravel\Socialite\Contracts\User as ProviderUserContract;
9
+use Wallo\FilamentCompanies\Contracts\CreatesConnectedAccounts;
10
+use Wallo\FilamentCompanies\Contracts\CreatesUserFromProvider;
11
+use Wallo\FilamentCompanies\Features;
12
+use Wallo\FilamentCompanies\FilamentCompanies;
13
+use Wallo\FilamentCompanies\Socialite;
14
+
15
+class CreateUserFromProvider implements CreatesUserFromProvider
16
+{
17
+    /**
18
+     * The creates connected accounts instance.
19
+     */
20
+    public CreatesConnectedAccounts $createsConnectedAccounts;
21
+
22
+    /**
23
+     * Create a new action instance.
24
+     */
25
+    public function __construct(CreatesConnectedAccounts $createsConnectedAccounts)
26
+    {
27
+        $this->createsConnectedAccounts = $createsConnectedAccounts;
28
+    }
29
+
30
+    /**
31
+     * Create a new user from a social provider user.
32
+     */
33
+    public function create(string $provider, ProviderUserContract $providerUser): User
34
+    {
35
+        return DB::transaction(function () use ($providerUser, $provider) {
36
+            return tap(User::create([
37
+                'name' => $providerUser->getName(),
38
+                'email' => $providerUser->getEmail(),
39
+            ]), function (User $user) use ($providerUser, $provider) {
40
+                $user->markEmailAsVerified();
41
+
42
+                if ($this->shouldSetProfilePhoto($providerUser)) {
43
+                    $user->setProfilePhotoFromUrl($providerUser->getAvatar());
44
+                }
45
+
46
+                $user->switchConnectedAccount(
47
+                    $this->createsConnectedAccounts->create($user, $provider, $providerUser)
48
+                );
49
+
50
+                $this->createCompany($user);
51
+            });
52
+        });
53
+    }
54
+
55
+    private function shouldSetProfilePhoto(ProviderUserContract $providerUser): bool
56
+    {
57
+        return Features::profilePhotos() &&
58
+            Socialite::hasProviderAvatarsFeature() &&
59
+            FilamentCompanies::managesProfilePhotos() &&
60
+            $providerUser->getAvatar();
61
+    }
62
+
63
+    /**
64
+     * Create a personal company for the user.
65
+     */
66
+    protected function createCompany(User $user): void
67
+    {
68
+        $user->ownedCompanies()->save(Company::forceCreate([
69
+            'user_id' => $user->id,
70
+            'name' => explode(' ', $user->name, 2)[0]."'s Company",
71
+            'personal_company' => true,
72
+        ]));
73
+    }
74
+}

+ 17
- 0
app/Actions/FilamentCompanies/DeleteCompany.php 查看文件

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use Wallo\FilamentCompanies\Contracts\DeletesCompanies;
7
+
8
+class DeleteCompany implements DeletesCompanies
9
+{
10
+    /**
11
+     * Delete the given company.
12
+     */
13
+    public function delete(Company $company): void
14
+    {
15
+        $company->purge();
16
+    }
17
+}

+ 51
- 0
app/Actions/FilamentCompanies/DeleteUser.php 查看文件

@@ -0,0 +1,51 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Support\Facades\DB;
8
+use Wallo\FilamentCompanies\Contracts\DeletesCompanies;
9
+use Wallo\FilamentCompanies\Contracts\DeletesUsers;
10
+
11
+class DeleteUser implements DeletesUsers
12
+{
13
+    /**
14
+     * The company deleter implementation.
15
+     */
16
+    protected DeletesCompanies $deletesCompanies;
17
+
18
+    /**
19
+     * Create a new action instance.
20
+     */
21
+    public function __construct(DeletesCompanies $deletesCompanies)
22
+    {
23
+        $this->deletesCompanies = $deletesCompanies;
24
+    }
25
+
26
+    /**
27
+     * Delete the given user.
28
+     */
29
+    public function delete(User $user): void
30
+    {
31
+        DB::transaction(function () use ($user) {
32
+            $this->deleteCompanies($user);
33
+            $user->deleteProfilePhoto();
34
+            $user->connectedAccounts->each(static fn ($account) => $account->delete());
35
+            $user->tokens->each(static fn ($token) => $token->delete());
36
+            $user->delete();
37
+        });
38
+    }
39
+
40
+    /**
41
+     * Delete the companies and company associations attached to the user.
42
+     */
43
+    protected function deleteCompanies(User $user): void
44
+    {
45
+        $user->companies()->detach();
46
+
47
+        $user->ownedCompanies->each(function (Company $company) {
48
+            $this->deletesCompanies->delete($company);
49
+        });
50
+    }
51
+}

+ 22
- 0
app/Actions/FilamentCompanies/HandleInvalidState.php 查看文件

@@ -0,0 +1,22 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use Illuminate\Http\Response;
6
+use Laravel\Socialite\Two\InvalidStateException;
7
+use Wallo\FilamentCompanies\Contracts\HandlesInvalidState;
8
+
9
+class HandleInvalidState implements HandlesInvalidState
10
+{
11
+    /**
12
+     * Handle an invalid state exception from a Socialite provider.
13
+     */
14
+    public function handle(InvalidStateException $exception, callable|null $callback = null): Response
15
+    {
16
+        if ($callback) {
17
+            return $callback($exception);
18
+        }
19
+
20
+        throw $exception;
21
+    }
22
+}

+ 91
- 0
app/Actions/FilamentCompanies/InviteCompanyEmployee.php 查看文件

@@ -0,0 +1,91 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Closure;
8
+use Illuminate\Auth\Access\AuthorizationException;
9
+use Illuminate\Database\Query\Builder;
10
+use Illuminate\Support\Facades\Gate;
11
+use Illuminate\Support\Facades\Mail;
12
+use Illuminate\Support\Facades\Validator;
13
+use Illuminate\Validation\Rule;
14
+use Wallo\FilamentCompanies\Contracts\InvitesCompanyEmployees;
15
+use Wallo\FilamentCompanies\Events\InvitingCompanyEmployee;
16
+use Wallo\FilamentCompanies\FilamentCompanies;
17
+use Wallo\FilamentCompanies\Mail\CompanyInvitation;
18
+use Wallo\FilamentCompanies\Rules\Role;
19
+
20
+class InviteCompanyEmployee implements InvitesCompanyEmployees
21
+{
22
+    /**
23
+     * Invite a new company employee to the given company.
24
+     *
25
+     * @throws AuthorizationException
26
+     */
27
+    public function invite(User $user, Company $company, string $email, string|null $role = null): void
28
+    {
29
+        Gate::forUser($user)->authorize('addCompanyEmployee', $company);
30
+
31
+        $this->validate($company, $email, $role);
32
+
33
+        InvitingCompanyEmployee::dispatch($company, $email, $role);
34
+
35
+        $invitation = $company->companyInvitations()->create([
36
+            'email' => $email,
37
+            'role' => $role,
38
+        ]);
39
+
40
+        Mail::to($email)->send(new CompanyInvitation($invitation));
41
+    }
42
+
43
+    /**
44
+     * Validate the invite employee operation.
45
+     */
46
+    protected function validate(Company $company, string $email, ?string $role): void
47
+    {
48
+        Validator::make([
49
+            'email' => $email,
50
+            'role' => $role,
51
+        ], $this->rules($company), [
52
+            'email.unique' => __('filament-companies::default.errors.employee_already_invited'),
53
+        ])->after(
54
+            $this->ensureUserIsNotAlreadyOnCompany($company, $email)
55
+        )->validateWithBag('addCompanyEmployee');
56
+    }
57
+
58
+    /**
59
+     * Get the validation rules for inviting a company employee.
60
+     *
61
+     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
62
+     */
63
+    protected function rules(Company $company): array
64
+    {
65
+        return array_filter([
66
+            'email' => [
67
+                'required', 'email',
68
+                Rule::unique('company_invitations')->where(static function (Builder $query) use ($company) {
69
+                    $query->where('company_id', $company->id);
70
+                }),
71
+            ],
72
+            'role' => FilamentCompanies::hasRoles()
73
+                            ? ['required', 'string', new Role]
74
+                            : null,
75
+        ]);
76
+    }
77
+
78
+    /**
79
+     * Ensure that the employee is not already on the company.
80
+     */
81
+    protected function ensureUserIsNotAlreadyOnCompany(Company $company, string $email): Closure
82
+    {
83
+        return static function ($validator) use ($company, $email) {
84
+            $validator->errors()->addIf(
85
+                $company->hasUserWithEmail($email),
86
+                'email',
87
+                __('filament-companies::default.errors.employee_already_belongs_to_company')
88
+            );
89
+        };
90
+    }
91
+}

+ 55
- 0
app/Actions/FilamentCompanies/RemoveCompanyEmployee.php 查看文件

@@ -0,0 +1,55 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Auth\Access\AuthorizationException;
8
+use Illuminate\Support\Facades\Gate;
9
+use Illuminate\Validation\ValidationException;
10
+use Wallo\FilamentCompanies\Contracts\RemovesCompanyEmployees;
11
+use Wallo\FilamentCompanies\Events\CompanyEmployeeRemoved;
12
+
13
+class RemoveCompanyEmployee implements RemovesCompanyEmployees
14
+{
15
+    /**
16
+     * Remove the company employee from the given company.
17
+     *
18
+     * @throws AuthorizationException
19
+     */
20
+    public function remove(User $user, Company $company, User $companyEmployee): void
21
+    {
22
+        $this->authorize($user, $company, $companyEmployee);
23
+
24
+        $this->ensureUserDoesNotOwnCompany($companyEmployee, $company);
25
+
26
+        $company->removeUser($companyEmployee);
27
+
28
+        CompanyEmployeeRemoved::dispatch($company, $companyEmployee);
29
+    }
30
+
31
+    /**
32
+     * Authorize that the user can remove the company employee.
33
+     *
34
+     * @throws AuthorizationException
35
+     */
36
+    protected function authorize(User $user, Company $company, User $companyEmployee): void
37
+    {
38
+        if (! Gate::forUser($user)->check('removeCompanyEmployee', $company) &&
39
+            $user->id !== $companyEmployee->id) {
40
+            throw new AuthorizationException;
41
+        }
42
+    }
43
+
44
+    /**
45
+     * Ensure that the currently authenticated user does not own the company.
46
+     */
47
+    protected function ensureUserDoesNotOwnCompany(User $companyEmployee, Company $company): void
48
+    {
49
+        if ($companyEmployee->id === $company->owner->id) {
50
+            throw ValidationException::withMessages([
51
+                'company' => [__('filament-companies::default.errors.cannot_leave_company')],
52
+            ])->errorBag('removeCompanyEmployee');
53
+        }
54
+    }
55
+}

+ 25
- 0
app/Actions/FilamentCompanies/ResolveSocialiteUser.php 查看文件

@@ -0,0 +1,25 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use Laravel\Socialite\Contracts\User;
6
+use Laravel\Socialite\Facades\Socialite;
7
+use Wallo\FilamentCompanies\Contracts\ResolvesSocialiteUsers;
8
+use Wallo\FilamentCompanies\Features;
9
+
10
+class ResolveSocialiteUser implements ResolvesSocialiteUsers
11
+{
12
+    /**
13
+     * Resolve the user for a given provider.
14
+     */
15
+    public function resolve(string $provider): User
16
+    {
17
+        $user = Socialite::driver($provider)->user();
18
+
19
+        if (Features::generatesMissingEmails()) {
20
+            $user->email = $user->getEmail() ?? ("{$user->id}@{$provider}".config('app.domain'));
21
+        }
22
+
23
+        return $user;
24
+    }
25
+}

+ 26
- 0
app/Actions/FilamentCompanies/SetUserPassword.php 查看文件

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\User;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
8
+use Laravel\Fortify\Rules\Password;
9
+use Wallo\FilamentCompanies\Contracts\SetsUserPasswords;
10
+
11
+class SetUserPassword implements SetsUserPasswords
12
+{
13
+    /**
14
+     * Validate and update the user's password.
15
+     */
16
+    public function set(User $user, array $input): void
17
+    {
18
+        Validator::make($input, [
19
+            'password' => ['required', 'string', new Password, 'confirmed'],
20
+        ])->validateWithBag('setPassword');
21
+
22
+        $user->forceFill([
23
+            'password' => Hash::make($input['password']),
24
+        ])->save();
25
+    }
26
+}

+ 33
- 0
app/Actions/FilamentCompanies/UpdateCompanyName.php 查看文件

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Auth\Access\AuthorizationException;
8
+use Illuminate\Support\Facades\Gate;
9
+use Illuminate\Support\Facades\Validator;
10
+use Wallo\FilamentCompanies\Contracts\UpdatesCompanyNames;
11
+
12
+class UpdateCompanyName implements UpdatesCompanyNames
13
+{
14
+    /**
15
+     * Validate and update the given company's name.
16
+     *
17
+     * @param  array<string, string>  $input
18
+     *
19
+     * @throws AuthorizationException
20
+     */
21
+    public function update(User $user, Company $company, array $input): void
22
+    {
23
+        Gate::forUser($user)->authorize('update', $company);
24
+
25
+        Validator::make($input, [
26
+            'name' => ['required', 'string', 'max:255'],
27
+        ])->validateWithBag('updateCompanyName');
28
+
29
+        $company->forceFill([
30
+            'name' => $input['name'],
31
+        ])->save();
32
+    }
33
+}

+ 37
- 0
app/Actions/FilamentCompanies/UpdateConnectedAccount.php 查看文件

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+namespace App\Actions\FilamentCompanies;
4
+
5
+use Illuminate\Auth\Access\AuthorizationException;
6
+use Illuminate\Support\Facades\Gate;
7
+use Laravel\Socialite\Contracts\User;
8
+use Wallo\FilamentCompanies\ConnectedAccount;
9
+use Wallo\FilamentCompanies\Contracts\UpdatesConnectedAccounts;
10
+
11
+class UpdateConnectedAccount implements UpdatesConnectedAccounts
12
+{
13
+    /**
14
+     * Update a given connected account.
15
+     *
16
+     * @throws AuthorizationException
17
+     */
18
+    public function update(mixed $user, ConnectedAccount $connectedAccount, string $provider, User $providerUser): ConnectedAccount
19
+    {
20
+        Gate::forUser($user)->authorize('update', $connectedAccount);
21
+
22
+        $connectedAccount->forceFill([
23
+            'provider' => strtolower($provider),
24
+            'provider_id' => $providerUser->getId(),
25
+            'name' => $providerUser->getName(),
26
+            'nickname' => $providerUser->getNickname(),
27
+            'email' => $providerUser->getEmail(),
28
+            'avatar_path' => $providerUser->getAvatar(),
29
+            'token' => $providerUser->token,
30
+            'secret' => $providerUser->tokenSecret ?? null,
31
+            'refresh_token' => $providerUser->refreshToken ?? null,
32
+            'expires_at' => property_exists($providerUser, 'expiresIn') ? now()->addSeconds($providerUser->expiresIn) : null,
33
+        ])->save();
34
+
35
+        return $connectedAccount;
36
+    }
37
+}

+ 53
- 0
app/Actions/Fortify/CreateNewUser.php 查看文件

@@ -0,0 +1,53 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Support\Facades\DB;
8
+use Illuminate\Support\Facades\Hash;
9
+use Illuminate\Support\Facades\Validator;
10
+use Laravel\Fortify\Contracts\CreatesNewUsers;
11
+use Wallo\FilamentCompanies\FilamentCompanies;
12
+
13
+class CreateNewUser implements CreatesNewUsers
14
+{
15
+    use PasswordValidationRules;
16
+
17
+    /**
18
+     * Create a newly registered user.
19
+     *
20
+     * @param  array<string, string>  $input
21
+     */
22
+    public function create(array $input): User
23
+    {
24
+        Validator::make($input, [
25
+            'name' => ['required', 'string', 'max:255'],
26
+            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
27
+            'password' => $this->passwordRules(),
28
+            'terms' => FilamentCompanies::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '',
29
+        ])->validate();
30
+
31
+        return DB::transaction(function () use ($input) {
32
+            return tap(User::create([
33
+                'name' => $input['name'],
34
+                'email' => $input['email'],
35
+                'password' => Hash::make($input['password']),
36
+            ]), function (User $user) {
37
+                $this->createCompany($user);
38
+            });
39
+        });
40
+    }
41
+
42
+    /**
43
+     * Create a personal company for the user.
44
+     */
45
+    protected function createCompany(User $user): void
46
+    {
47
+        $user->ownedCompanies()->save(Company::forceCreate([
48
+            'user_id' => $user->id,
49
+            'name' => explode(' ', $user->name, 2)[0]."'s Company",
50
+            'personal_company' => true,
51
+        ]));
52
+    }
53
+}

+ 18
- 0
app/Actions/Fortify/PasswordValidationRules.php 查看文件

@@ -0,0 +1,18 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use Laravel\Fortify\Rules\Password;
6
+
7
+trait PasswordValidationRules
8
+{
9
+    /**
10
+     * Get the validation rules used to validate passwords.
11
+     *
12
+     * @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
13
+     */
14
+    protected function passwordRules(): array
15
+    {
16
+        return ['required', 'string', new Password, 'confirmed'];
17
+    }
18
+}

+ 29
- 0
app/Actions/Fortify/ResetUserPassword.php 查看文件

@@ -0,0 +1,29 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
8
+use Laravel\Fortify\Contracts\ResetsUserPasswords;
9
+
10
+class ResetUserPassword implements ResetsUserPasswords
11
+{
12
+    use PasswordValidationRules;
13
+
14
+    /**
15
+     * Validate and reset the user's forgotten password.
16
+     *
17
+     * @param  array<string, string>  $input
18
+     */
19
+    public function reset(User $user, array $input): void
20
+    {
21
+        Validator::make($input, [
22
+            'password' => $this->passwordRules(),
23
+        ])->validate();
24
+
25
+        $user->forceFill([
26
+            'password' => Hash::make($input['password']),
27
+        ])->save();
28
+    }
29
+}

+ 32
- 0
app/Actions/Fortify/UpdateUserPassword.php 查看文件

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Facades\Validator;
8
+use Laravel\Fortify\Contracts\UpdatesUserPasswords;
9
+
10
+class UpdateUserPassword implements UpdatesUserPasswords
11
+{
12
+    use PasswordValidationRules;
13
+
14
+    /**
15
+     * Validate and update the user's password.
16
+     *
17
+     * @param  array<string, string>  $input
18
+     */
19
+    public function update(User $user, array $input): void
20
+    {
21
+        Validator::make($input, [
22
+            'current_password' => ['required', 'string', 'current_password:web'],
23
+            'password' => $this->passwordRules(),
24
+        ], [
25
+            'current_password.current_password' => __('filament-companies::default.errors.password_does_not_match'),
26
+        ])->validateWithBag('updatePassword');
27
+
28
+        $user->forceFill([
29
+            'password' => Hash::make($input['password']),
30
+        ])->save();
31
+    }
32
+}

+ 64
- 0
app/Actions/Fortify/UpdateUserProfileInformation.php 查看文件

@@ -0,0 +1,64 @@
1
+<?php
2
+
3
+namespace App\Actions\Fortify;
4
+
5
+use App\Models\User;
6
+use Illuminate\Contracts\Auth\MustVerifyEmail;
7
+use Illuminate\Support\Facades\Validator;
8
+use Illuminate\Validation\Rule;
9
+use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
10
+
11
+class UpdateUserProfileInformation implements UpdatesUserProfileInformation
12
+{
13
+    /**
14
+     * Validate and update the given user's profile information.
15
+     *
16
+     * @param  array<string, string>  $input
17
+     */
18
+    public function update(User $user, array $input): void
19
+    {
20
+        Validator::make($input, [
21
+            'name' => ['required', 'string', 'max:255'],
22
+            'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
23
+            'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
24
+        ])->validateWithBag('updateProfileInformation');
25
+
26
+        if (isset($input['photo'])) {
27
+            $user->updateProfilePhoto($input['photo']);
28
+        }
29
+
30
+        if ($input['email'] !== $user->email &&
31
+            $this->userMustVerifyEmail()) {
32
+            $this->updateVerifiedUser($user, $input);
33
+        } else {
34
+            $user->forceFill([
35
+                'name' => $input['name'],
36
+                'email' => $input['email'],
37
+            ])->save();
38
+        }
39
+    }
40
+
41
+    /**
42
+     * Determine if the user must verify their email address.
43
+     */
44
+    protected function userMustVerifyEmail(): bool
45
+    {
46
+        return in_array(MustVerifyEmail::class, class_implements(User::class));
47
+    }
48
+
49
+    /**
50
+     * Update the given verified user's profile information.
51
+     *
52
+     * @param  array<string, string>  $input
53
+     */
54
+    protected function updateVerifiedUser(User $user, array $input): void
55
+    {
56
+        $user->forceFill([
57
+            'name' => $input['name'],
58
+            'email' => $input['email'],
59
+            'email_verified_at' => null,
60
+        ])->save();
61
+
62
+        $user->sendEmailVerificationNotification();
63
+    }
64
+}

+ 1
- 19
app/Exceptions/Handler.php 查看文件

@@ -8,25 +8,7 @@ use Throwable;
8 8
 class Handler extends ExceptionHandler
9 9
 {
10 10
     /**
11
-     * A list of exception types with their corresponding custom log levels.
12
-     *
13
-     * @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
14
-     */
15
-    protected $levels = [
16
-        //
17
-    ];
18
-
19
-    /**
20
-     * A list of the exception types that are not reported.
21
-     *
22
-     * @var array<int, class-string<\Throwable>>
23
-     */
24
-    protected $dontReport = [
25
-        //
26
-    ];
27
-
28
-    /**
29
-     * A list of the inputs that are never flashed to the session on validation exceptions.
11
+     * The list of the inputs that are never flashed to the session on validation exceptions.
30 12
      *
31 13
      * @var array<int, string>
32 14
      */

+ 30
- 0
app/Filament/Pages/Companies.php 查看文件

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages;
4
+
5
+use Filament\Pages\Page;
6
+use Illuminate\Support\Facades\Auth;
7
+
8
+class Companies extends Page
9
+{
10
+    protected static ?string $navigationIcon = 'heroicon-o-office-building';
11
+
12
+    protected static string $view = 'filament.pages.companies';
13
+
14
+    protected static function shouldRegisterNavigation(): bool
15
+    {
16
+        return Auth::user()->currentCompany->name === 'ERPSAAS';
17
+    }
18
+
19
+    public function mount(): void
20
+    {
21
+        abort_unless(Auth::user()->currentCompany->name === 'ERPSAAS', 403);
22
+    }
23
+
24
+    protected function getHeaderWidgets(): array
25
+    {
26
+        return [
27
+            \App\Filament\Pages\Widgets\Companies::class,
28
+        ];
29
+    }
30
+}

+ 10
- 0
app/Filament/Pages/Dashboard.php 查看文件

@@ -0,0 +1,10 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages;
4
+
5
+use Filament\Pages\Dashboard as BasePage;
6
+
7
+class Dashboard extends BasePage
8
+{
9
+    //
10
+}

+ 30
- 0
app/Filament/Pages/Users.php 查看文件

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages;
4
+
5
+use Filament\Pages\Page;
6
+use Illuminate\Support\Facades\Auth;
7
+
8
+class Users extends Page
9
+{
10
+    protected static ?string $navigationIcon = 'heroicon-o-user-group';
11
+
12
+    protected static string $view = 'filament.pages.users';
13
+
14
+    protected static function shouldRegisterNavigation(): bool
15
+    {
16
+        return Auth::user()->currentCompany->name === 'ERPSAAS';
17
+    }
18
+
19
+    public function mount(): void
20
+    {
21
+        abort_unless(Auth::user()->currentCompany->name === 'ERPSAAS', 403);
22
+    }
23
+
24
+    protected function getHeaderWidgets(): array
25
+    {
26
+        return [
27
+            \App\Filament\Pages\Widgets\Users::class,
28
+        ];
29
+    }
30
+}

+ 72
- 0
app/Filament/Pages/Widgets/Companies.php 查看文件

@@ -0,0 +1,72 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages\Widgets;
4
+
5
+use App\Models\Company;
6
+use Closure;
7
+use Exception;
8
+use Filament\Tables;
9
+use Filament\Widgets\TableWidget as PageWidget;
10
+use Illuminate\Contracts\Support\Htmlable;
11
+use Illuminate\Database\Eloquent\Builder;
12
+use Illuminate\Database\Eloquent\Relations\Relation;
13
+
14
+class Companies extends PageWidget
15
+{
16
+    protected int|string|array $columnSpan = [
17
+        'md' => 2,
18
+        'xl' => 3,
19
+    ];
20
+
21
+    protected function getTableQuery(): Builder|Relation
22
+    {
23
+        return Company::query();
24
+    }
25
+
26
+    protected function getTableHeading(): string|Htmlable|Closure|null
27
+    {
28
+        return null;
29
+    }
30
+
31
+    /**
32
+     * @throws Exception
33
+     */
34
+    protected function getTableFilters(): array
35
+    {
36
+        return [
37
+            Tables\Filters\SelectFilter::make('name')
38
+                ->label('Owner')
39
+                ->relationship('owner', 'name'),
40
+            Tables\Filters\TernaryFilter::make('personal_company')
41
+                ->label('Personal Company')
42
+        ];
43
+    }
44
+
45
+    protected function getTableColumns(): array
46
+    {
47
+        return [
48
+            Tables\Columns\ViewColumn::make('owner.name')
49
+                ->view('filament.components.companies.avatar-column')
50
+                ->label('Owner')
51
+                ->sortable()
52
+                ->grow(false),
53
+            Tables\Columns\TextColumn::make('name')
54
+                ->weight('semibold')
55
+                ->label('Company')
56
+                ->sortable(),
57
+            Tables\Columns\TextColumn::make('users_count')
58
+                ->label('Employees')
59
+                ->weight('semibold')
60
+                ->counts('users')
61
+                ->sortable(),
62
+            Tables\Columns\IconColumn::make('personal_company')
63
+                ->label('Personal Company')
64
+                ->boolean()
65
+                ->sortable()
66
+                ->trueIcon('heroicon-o-badge-check')
67
+                ->falseIcon('heroicon-o-x-circle')
68
+                ->trueColor('primary')
69
+                ->falseColor('secondary')
70
+        ];
71
+    }
72
+}

+ 44
- 0
app/Filament/Pages/Widgets/Users.php 查看文件

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages\Widgets;
4
+
5
+use App\Models\User;
6
+use Closure;
7
+use Filament\Tables;
8
+use Filament\Widgets\TableWidget as PageWidget;
9
+use Illuminate\Contracts\Support\Htmlable;
10
+use Illuminate\Database\Eloquent\Builder;
11
+use Illuminate\Database\Eloquent\Relations\Relation;
12
+
13
+class Users extends PageWidget
14
+{
15
+    protected int|string|array $columnSpan = [
16
+        'md' => 2,
17
+        'xl' => 3,
18
+    ];
19
+
20
+    protected function getTableQuery(): Builder|Relation
21
+    {
22
+        return User::query();
23
+    }
24
+
25
+    protected function getTableHeading(): string|Htmlable|Closure|null
26
+    {
27
+        return null;
28
+    }
29
+
30
+    protected function getTableColumns(): array
31
+    {
32
+        return [
33
+            Tables\Columns\ViewColumn::make('name')
34
+                ->view('filament.components.users.avatar-column')
35
+                ->label('Name')
36
+                ->sortable()
37
+                ->grow(false),
38
+            Tables\Columns\TextColumn::make('owned_companies')
39
+                ->label('Companies')
40
+                ->weight('semibold')
41
+                ->getStateUsing(static fn ($record) => $record->ownedCompanies->count()),
42
+        ];
43
+    }
44
+}

+ 1
- 1
app/Http/Kernel.php 查看文件

@@ -48,7 +48,7 @@ class Kernel extends HttpKernel
48 48
     /**
49 49
      * The application's middleware aliases.
50 50
      *
51
-     * Aliases may be used to conveniently assign middleware to routes and groups.
51
+     * Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
52 52
      *
53 53
      * @var array<string, class-string|string>
54 54
      */

+ 44
- 0
app/Models/Company.php 查看文件

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Factories\HasFactory;
6
+use Wallo\FilamentCompanies\Company as FilamentCompaniesCompany;
7
+use Wallo\FilamentCompanies\Events\CompanyCreated;
8
+use Wallo\FilamentCompanies\Events\CompanyDeleted;
9
+use Wallo\FilamentCompanies\Events\CompanyUpdated;
10
+
11
+class Company extends FilamentCompaniesCompany
12
+{
13
+    use HasFactory;
14
+
15
+    /**
16
+     * The attributes that should be cast.
17
+     *
18
+     * @var array<string, string>
19
+     */
20
+    protected $casts = [
21
+        'personal_company' => 'boolean',
22
+    ];
23
+
24
+    /**
25
+     * The attributes that are mass assignable.
26
+     *
27
+     * @var array<int, string>
28
+     */
29
+    protected $fillable = [
30
+        'name',
31
+        'personal_company',
32
+    ];
33
+
34
+    /**
35
+     * The event map for the model.
36
+     *
37
+     * @var array<string, class-string>
38
+     */
39
+    protected $dispatchesEvents = [
40
+        'created' => CompanyCreated::class,
41
+        'updated' => CompanyUpdated::class,
42
+        'deleted' => CompanyDeleted::class,
43
+    ];
44
+}

+ 28
- 0
app/Models/CompanyInvitation.php 查看文件

@@ -0,0 +1,28 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Model;
6
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
7
+use Wallo\FilamentCompanies\FilamentCompanies;
8
+
9
+class CompanyInvitation extends Model
10
+{
11
+    /**
12
+     * The attributes that are mass assignable.
13
+     *
14
+     * @var array<int, string>
15
+     */
16
+    protected $fillable = [
17
+        'email',
18
+        'role',
19
+    ];
20
+
21
+    /**
22
+     * Get the company that the invitation belongs to.
23
+     */
24
+    public function company(): BelongsTo
25
+    {
26
+        return $this->belongsTo(FilamentCompanies::companyModel());
27
+    }
28
+}

+ 42
- 0
app/Models/ConnectedAccount.php 查看文件

@@ -0,0 +1,42 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Concerns\HasTimestamps;
6
+use Wallo\FilamentCompanies\ConnectedAccount as SocialiteConnectedAccount;
7
+use Wallo\FilamentCompanies\Events\ConnectedAccountCreated;
8
+use Wallo\FilamentCompanies\Events\ConnectedAccountDeleted;
9
+use Wallo\FilamentCompanies\Events\ConnectedAccountUpdated;
10
+
11
+class ConnectedAccount extends SocialiteConnectedAccount
12
+{
13
+    use HasTimestamps;
14
+
15
+    /**
16
+     * The attributes that are mass assignable.
17
+     *
18
+     * @var array<int, string>
19
+     */
20
+    protected $fillable = [
21
+        'provider',
22
+        'provider_id',
23
+        'name',
24
+        'nickname',
25
+        'email',
26
+        'avatar_path',
27
+        'token',
28
+        'refresh_token',
29
+        'expires_at',
30
+    ];
31
+
32
+    /**
33
+     * The event map for the model.
34
+     *
35
+     * @var array<string, class-string>
36
+     */
37
+    protected $dispatchesEvents = [
38
+        'created' => ConnectedAccountCreated::class,
39
+        'updated' => ConnectedAccountUpdated::class,
40
+        'deleted' => ConnectedAccountDeleted::class,
41
+    ];
42
+}

+ 15
- 0
app/Models/Employeeship.php 查看文件

@@ -0,0 +1,15 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Wallo\FilamentCompanies\Employeeship as FilamentCompaniesEmployeeship;
6
+
7
+class Employeeship extends FilamentCompaniesEmployeeship
8
+{
9
+    /**
10
+     * Indicates if the IDs are auto-incrementing.
11
+     *
12
+     * @var bool
13
+     */
14
+    public $incrementing = true;
15
+}

+ 38
- 6
app/Models/User.php 查看文件

@@ -2,15 +2,38 @@
2 2
 
3 3
 namespace App\Models;
4 4
 
5
-// use Illuminate\Contracts\Auth\MustVerifyEmail;
5
+use Filament\Models\Contracts\FilamentUser;
6
+use Filament\Models\Contracts\HasAvatar;
6 7
 use Illuminate\Database\Eloquent\Factories\HasFactory;
7 8
 use Illuminate\Foundation\Auth\User as Authenticatable;
8 9
 use Illuminate\Notifications\Notifiable;
10
+use Laravel\Fortify\TwoFactorAuthenticatable;
9 11
 use Laravel\Sanctum\HasApiTokens;
12
+use Wallo\FilamentCompanies\HasCompanies;
13
+use Wallo\FilamentCompanies\HasConnectedAccounts;
14
+use Wallo\FilamentCompanies\HasProfilePhoto;
15
+use Wallo\FilamentCompanies\SetsProfilePhotoFromUrl;
10 16
 
11
-class User extends Authenticatable
17
+class User extends Authenticatable implements FilamentUser, HasAvatar
12 18
 {
13
-    use HasApiTokens, HasFactory, Notifiable;
19
+    use HasApiTokens;
20
+    use HasFactory;
21
+    use HasProfilePhoto;
22
+    use HasCompanies;
23
+    use HasConnectedAccounts;
24
+    use Notifiable;
25
+    use SetsProfilePhotoFromUrl;
26
+    use TwoFactorAuthenticatable;
27
+
28
+    public function canAccessFilament(): bool
29
+    {
30
+        return true;
31
+    }
32
+
33
+    public function getFilamentAvatarUrl(): ?string
34
+    {
35
+        return $this->profile_photo_url;
36
+    }
14 37
 
15 38
     /**
16 39
      * The attributes that are mass assignable.
@@ -18,9 +41,7 @@ class User extends Authenticatable
18 41
      * @var array<int, string>
19 42
      */
20 43
     protected $fillable = [
21
-        'name',
22
-        'email',
23
-        'password',
44
+        'name', 'email', 'password',
24 45
     ];
25 46
 
26 47
     /**
@@ -31,6 +52,8 @@ class User extends Authenticatable
31 52
     protected $hidden = [
32 53
         'password',
33 54
         'remember_token',
55
+        'two_factor_recovery_codes',
56
+        'two_factor_secret',
34 57
     ];
35 58
 
36 59
     /**
@@ -41,4 +64,13 @@ class User extends Authenticatable
41 64
     protected $casts = [
42 65
         'email_verified_at' => 'datetime',
43 66
     ];
67
+
68
+    /**
69
+     * The accessors to append to the model's array form.
70
+     *
71
+     * @var array<int, string>
72
+     */
73
+    protected $appends = [
74
+        'profile_photo_url',
75
+    ];
44 76
 }

+ 76
- 0
app/Policies/CompanyPolicy.php 查看文件

@@ -0,0 +1,76 @@
1
+<?php
2
+
3
+namespace App\Policies;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Auth\Access\HandlesAuthorization;
8
+
9
+class CompanyPolicy
10
+{
11
+    use HandlesAuthorization;
12
+
13
+    /**
14
+     * Determine whether the user can view any models.
15
+     */
16
+    public function viewAny(User $user): bool
17
+    {
18
+        return true;
19
+    }
20
+
21
+    /**
22
+     * Determine whether the user can view the model.
23
+     */
24
+    public function view(User $user, Company $company): bool
25
+    {
26
+        return $user->belongsToCompany($company);
27
+    }
28
+
29
+    /**
30
+     * Determine whether the user can create models.
31
+     */
32
+    public function create(User $user): bool
33
+    {
34
+        return true;
35
+    }
36
+
37
+    /**
38
+     * Determine whether the user can update the model.
39
+     */
40
+    public function update(User $user, Company $company): bool
41
+    {
42
+        return $user->ownsCompany($company);
43
+    }
44
+
45
+    /**
46
+     * Determine whether the user can add company employees.
47
+     */
48
+    public function addCompanyEmployee(User $user, Company $company): bool
49
+    {
50
+        return $user->ownsCompany($company);
51
+    }
52
+
53
+    /**
54
+     * Determine whether the user can update company employee permissions.
55
+     */
56
+    public function updateCompanyEmployee(User $user, Company $company): bool
57
+    {
58
+        return $user->ownsCompany($company);
59
+    }
60
+
61
+    /**
62
+     * Determine whether the user can remove company employees.
63
+     */
64
+    public function removeCompanyEmployee(User $user, Company $company): bool
65
+    {
66
+        return $user->ownsCompany($company);
67
+    }
68
+
69
+    /**
70
+     * Determine whether the user can delete the model.
71
+     */
72
+    public function delete(User $user, Company $company): bool
73
+    {
74
+        return $user->ownsCompany($company);
75
+    }
76
+}

+ 52
- 0
app/Policies/ConnectedAccountPolicy.php 查看文件

@@ -0,0 +1,52 @@
1
+<?php
2
+
3
+namespace App\Policies;
4
+
5
+use App\Models\ConnectedAccount;
6
+use App\Models\User;
7
+use Illuminate\Auth\Access\HandlesAuthorization;
8
+
9
+class ConnectedAccountPolicy
10
+{
11
+    use HandlesAuthorization;
12
+
13
+    /**
14
+     * Determine whether the user can view any models.
15
+     */
16
+    public function viewAny(User $user): bool
17
+    {
18
+        return true;
19
+    }
20
+
21
+    /**
22
+     * Determine whether the user can view the model.
23
+     */
24
+    public function view(User $user, ConnectedAccount $connectedAccount): bool
25
+    {
26
+        return $user->ownsConnectedAccount($connectedAccount);
27
+    }
28
+
29
+    /**
30
+     * Determine whether the user can create models.
31
+     */
32
+    public function create(User $user): bool
33
+    {
34
+        return true;
35
+    }
36
+
37
+    /**
38
+     * Determine whether the user can update the model.
39
+     */
40
+    public function update(User $user, ConnectedAccount $connectedAccount): bool
41
+    {
42
+        return $user->ownsConnectedAccount($connectedAccount);
43
+    }
44
+
45
+    /**
46
+     * Determine whether the user can delete the model.
47
+     */
48
+    public function delete(User $user, ConnectedAccount $connectedAccount): bool
49
+    {
50
+        return $user->ownsConnectedAccount($connectedAccount);
51
+    }
52
+}

+ 4
- 1
app/Providers/AppServiceProvider.php 查看文件

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Providers;
4 4
 
5
+use Filament\Facades\Filament;
5 6
 use Illuminate\Support\ServiceProvider;
6 7
 
7 8
 class AppServiceProvider extends ServiceProvider
@@ -19,6 +20,8 @@ class AppServiceProvider extends ServiceProvider
19 20
      */
20 21
     public function boot(): void
21 22
     {
22
-        //
23
+        Filament::serving(static function () {
24
+            Filament::registerViteTheme('resources/css/filament.css');
25
+        });
23 26
     }
24 27
 }

+ 1
- 1
app/Providers/AuthServiceProvider.php 查看文件

@@ -13,7 +13,7 @@ class AuthServiceProvider extends ServiceProvider
13 13
      * @var array<class-string, class-string>
14 14
      */
15 15
     protected $policies = [
16
-        // 'App\Models\Model' => 'App\Policies\ModelPolicy',
16
+        //
17 17
     ];
18 18
 
19 19
     /**

+ 132
- 0
app/Providers/FilamentCompaniesServiceProvider.php 查看文件

@@ -0,0 +1,132 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use App\Actions\FilamentCompanies\AddCompanyEmployee;
6
+use App\Actions\FilamentCompanies\CreateCompany;
7
+use App\Actions\FilamentCompanies\CreateConnectedAccount;
8
+use App\Actions\FilamentCompanies\CreateUserFromProvider;
9
+use App\Actions\FilamentCompanies\DeleteCompany;
10
+use App\Actions\FilamentCompanies\DeleteUser;
11
+use App\Actions\FilamentCompanies\HandleInvalidState;
12
+use App\Actions\FilamentCompanies\InviteCompanyEmployee;
13
+use App\Actions\FilamentCompanies\RemoveCompanyEmployee;
14
+use App\Actions\FilamentCompanies\ResolveSocialiteUser;
15
+use App\Actions\FilamentCompanies\SetUserPassword;
16
+use App\Actions\FilamentCompanies\UpdateCompanyName;
17
+use App\Actions\FilamentCompanies\UpdateConnectedAccount;
18
+use Filament\Facades\Filament;
19
+use Filament\Navigation\UserMenuItem;
20
+use Illuminate\Http\RedirectResponse;
21
+use Illuminate\Support\Facades\Blade;
22
+use Illuminate\Support\ServiceProvider;
23
+use Wallo\FilamentCompanies\Actions\GenerateRedirectForProvider;
24
+use Wallo\FilamentCompanies\FilamentCompanies;
25
+use Wallo\FilamentCompanies\Pages\User\APITokens;
26
+use Wallo\FilamentCompanies\Pages\User\Profile;
27
+use Wallo\FilamentCompanies\Socialite;
28
+
29
+class FilamentCompaniesServiceProvider extends ServiceProvider
30
+{
31
+    /**
32
+     * Register any application services.
33
+     */
34
+    public function register(): void
35
+    {
36
+        //
37
+    }
38
+
39
+    /**
40
+     * Bootstrap any application services.
41
+     */
42
+    public function boot(): void
43
+    {
44
+        if (FilamentCompanies::hasCompanyFeatures()) {
45
+            Filament::registerRenderHook(
46
+                'global-search.end',
47
+                static fn (): string => Blade::render('<x-filament-companies::dropdown.navigation-menu />'),
48
+            );
49
+        }
50
+
51
+        Filament::serving(static function () {
52
+            Filament::registerUserMenuItems([
53
+                'account' => UserMenuItem::make()->url(Profile::getUrl()),
54
+            ]);
55
+        });
56
+
57
+        if (FilamentCompanies::hasApiFeatures()) {
58
+            Filament::serving(static function () {
59
+                Filament::registerUserMenuItems([
60
+                    UserMenuItem::make()
61
+                        ->label('API Tokens')
62
+                        ->icon('heroicon-s-lock-open')
63
+                        ->url(APITokens::getUrl()),
64
+                ]);
65
+            });
66
+        }
67
+
68
+        Filament::serving(static function () {
69
+            Filament::registerUserMenuItems([
70
+                'logout' => UserMenuItem::make()->url(route('logout')),
71
+            ]);
72
+        });
73
+
74
+        RedirectResponse::macro('banner', function ($message) {
75
+            return $this->with('flash', [
76
+                'bannerStyle' => 'success',
77
+                'banner' => $message,
78
+            ]);
79
+        });
80
+
81
+        RedirectResponse::macro('dangerBanner', function ($message) {
82
+            return $this->with('flash', [
83
+                'bannerStyle' => 'danger',
84
+                'banner' => $message,
85
+            ]);
86
+        });
87
+
88
+        Filament::registerRenderHook(
89
+            'content.start',
90
+            static fn (): string => Blade::render('<x-filament-companies::banner />'),
91
+        );
92
+
93
+        $this->configurePermissions();
94
+
95
+        FilamentCompanies::createCompaniesUsing(CreateCompany::class);
96
+        FilamentCompanies::updateCompanyNamesUsing(UpdateCompanyName::class);
97
+        FilamentCompanies::addCompanyEmployeesUsing(AddCompanyEmployee::class);
98
+        FilamentCompanies::inviteCompanyEmployeesUsing(InviteCompanyEmployee::class);
99
+        FilamentCompanies::removeCompanyEmployeesUsing(RemoveCompanyEmployee::class);
100
+        FilamentCompanies::deleteCompaniesUsing(DeleteCompany::class);
101
+        FilamentCompanies::deleteUsersUsing(DeleteUser::class);
102
+
103
+        Socialite::resolvesSocialiteUsersUsing(ResolveSocialiteUser::class);
104
+        Socialite::createUsersFromProviderUsing(CreateUserFromProvider::class);
105
+        Socialite::createConnectedAccountsUsing(CreateConnectedAccount::class);
106
+        Socialite::updateConnectedAccountsUsing(UpdateConnectedAccount::class);
107
+        Socialite::setUserPasswordsUsing(SetUserPassword::class);
108
+        Socialite::handlesInvalidStateUsing(HandleInvalidState::class);
109
+        Socialite::generatesProvidersRedirectsUsing(GenerateRedirectForProvider::class);
110
+    }
111
+
112
+    /**
113
+     * Configure the roles and permissions that are available within the application.
114
+     */
115
+    protected function configurePermissions(): void
116
+    {
117
+        FilamentCompanies::defaultApiTokenPermissions(['read']);
118
+
119
+        FilamentCompanies::role('admin', 'Administrator', [
120
+            'create',
121
+            'read',
122
+            'update',
123
+            'delete',
124
+        ])->description('Administrator users can perform any action.');
125
+
126
+        FilamentCompanies::role('editor', 'Editor', [
127
+            'read',
128
+            'create',
129
+            'update',
130
+        ])->description('Editor users have the ability to read, create, and update.');
131
+    }
132
+}

+ 45
- 0
app/Providers/FortifyServiceProvider.php 查看文件

@@ -0,0 +1,45 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use App\Actions\Fortify\CreateNewUser;
6
+use App\Actions\Fortify\ResetUserPassword;
7
+use App\Actions\Fortify\UpdateUserPassword;
8
+use App\Actions\Fortify\UpdateUserProfileInformation;
9
+use Illuminate\Cache\RateLimiting\Limit;
10
+use Illuminate\Http\Request;
11
+use Illuminate\Support\Facades\RateLimiter;
12
+use Illuminate\Support\ServiceProvider;
13
+use Laravel\Fortify\Fortify;
14
+
15
+class FortifyServiceProvider extends ServiceProvider
16
+{
17
+    /**
18
+     * Register any application services.
19
+     */
20
+    public function register(): void
21
+    {
22
+        //
23
+    }
24
+
25
+    /**
26
+     * Bootstrap any application services.
27
+     */
28
+    public function boot(): void
29
+    {
30
+        Fortify::createUsersUsing(CreateNewUser::class);
31
+        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
32
+        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
33
+        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
34
+
35
+        RateLimiter::for('login', function (Request $request) {
36
+            $email = (string) $request->email;
37
+
38
+            return Limit::perMinute(5)->by($email.$request->ip());
39
+        });
40
+
41
+        RateLimiter::for('two-factor', function (Request $request) {
42
+            return Limit::perMinute(5)->by($request->session()->get('login.id'));
43
+        });
44
+    }
45
+}

+ 5
- 13
app/Providers/RouteServiceProvider.php 查看文件

@@ -11,20 +11,22 @@ use Illuminate\Support\Facades\Route;
11 11
 class RouteServiceProvider extends ServiceProvider
12 12
 {
13 13
     /**
14
-     * The path to the "home" route for your application.
14
+     * The path to your application's "home" route.
15 15
      *
16 16
      * Typically, users are redirected here after authentication.
17 17
      *
18 18
      * @var string
19 19
      */
20
-    public const HOME = '/home';
20
+    public const HOME = '';
21 21
 
22 22
     /**
23 23
      * Define your route model bindings, pattern filters, and other route configuration.
24 24
      */
25 25
     public function boot(): void
26 26
     {
27
-        $this->configureRateLimiting();
27
+        RateLimiter::for('api', function (Request $request) {
28
+            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
29
+        });
28 30
 
29 31
         $this->routes(function () {
30 32
             Route::middleware('api')
@@ -35,14 +37,4 @@ class RouteServiceProvider extends ServiceProvider
35 37
                 ->group(base_path('routes/web.php'));
36 38
         });
37 39
     }
38
-
39
-    /**
40
-     * Configure the rate limiters for the application.
41
-     */
42
-    protected function configureRateLimiting(): void
43
-    {
44
-        RateLimiter::for('api', function (Request $request) {
45
-            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
46
-        });
47
-    }
48 40
 }

+ 7
- 3
composer.json 查看文件

@@ -6,18 +6,21 @@
6 6
     "license": "MIT",
7 7
     "require": {
8 8
         "php": "^8.1",
9
+        "andrewdwallo/filament-companies": "^2.0",
10
+        "filament/filament": "^2.17",
9 11
         "guzzlehttp/guzzle": "^7.2",
10
-        "laravel/framework": "^10.0",
12
+        "laravel/framework": "^10.8",
11 13
         "laravel/sanctum": "^3.2",
12 14
         "laravel/tinker": "^2.8"
13 15
     },
14 16
     "require-dev": {
17
+        "doctrine/dbal": "^3.6",
15 18
         "fakerphp/faker": "^1.9.1",
16 19
         "laravel/pint": "^1.0",
17 20
         "laravel/sail": "^1.18",
18 21
         "mockery/mockery": "^1.4.4",
19 22
         "nunomaduro/collision": "^7.0",
20
-        "phpunit/phpunit": "^10.0",
23
+        "phpunit/phpunit": "^10.1",
21 24
         "spatie/laravel-ignition": "^2.0"
22 25
     },
23 26
     "autoload": {
@@ -38,7 +41,8 @@
38 41
             "@php artisan package:discover --ansi"
39 42
         ],
40 43
         "post-update-cmd": [
41
-            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
44
+            "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
45
+            "@php artisan filament:upgrade"
42 46
         ],
43 47
         "post-root-package-install": [
44 48
             "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""

+ 2536
- 440
composer.lock
文件差异内容过多而无法显示
查看文件


+ 6
- 31
config/app.php 查看文件

@@ -1,6 +1,7 @@
1 1
 <?php
2 2
 
3 3
 use Illuminate\Support\Facades\Facade;
4
+use Illuminate\Support\ServiceProvider;
4 5
 
5 6
 return [
6 7
 
@@ -154,34 +155,7 @@ return [
154 155
     |
155 156
     */
156 157
 
157
-    'providers' => [
158
-
159
-        /*
160
-         * Laravel Framework Service Providers...
161
-         */
162
-        Illuminate\Auth\AuthServiceProvider::class,
163
-        Illuminate\Broadcasting\BroadcastServiceProvider::class,
164
-        Illuminate\Bus\BusServiceProvider::class,
165
-        Illuminate\Cache\CacheServiceProvider::class,
166
-        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
167
-        Illuminate\Cookie\CookieServiceProvider::class,
168
-        Illuminate\Database\DatabaseServiceProvider::class,
169
-        Illuminate\Encryption\EncryptionServiceProvider::class,
170
-        Illuminate\Filesystem\FilesystemServiceProvider::class,
171
-        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
172
-        Illuminate\Hashing\HashServiceProvider::class,
173
-        Illuminate\Mail\MailServiceProvider::class,
174
-        Illuminate\Notifications\NotificationServiceProvider::class,
175
-        Illuminate\Pagination\PaginationServiceProvider::class,
176
-        Illuminate\Pipeline\PipelineServiceProvider::class,
177
-        Illuminate\Queue\QueueServiceProvider::class,
178
-        Illuminate\Redis\RedisServiceProvider::class,
179
-        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
180
-        Illuminate\Session\SessionServiceProvider::class,
181
-        Illuminate\Translation\TranslationServiceProvider::class,
182
-        Illuminate\Validation\ValidationServiceProvider::class,
183
-        Illuminate\View\ViewServiceProvider::class,
184
-
158
+    'providers' => ServiceProvider::defaultProviders()->merge([
185 159
         /*
186 160
          * Package Service Providers...
187 161
          */
@@ -194,8 +168,9 @@ return [
194 168
         // App\Providers\BroadcastServiceProvider::class,
195 169
         App\Providers\EventServiceProvider::class,
196 170
         App\Providers\RouteServiceProvider::class,
197
-
198
-    ],
171
+        App\Providers\FortifyServiceProvider::class,
172
+        App\Providers\FilamentCompaniesServiceProvider::class,
173
+    ])->toArray(),
199 174
 
200 175
     /*
201 176
     |--------------------------------------------------------------------------
@@ -209,7 +184,7 @@ return [
209 184
     */
210 185
 
211 186
     'aliases' => Facade::defaultAliases()->merge([
212
-        // 'ExampleClass' => App\Example\ExampleClass::class,
187
+        // 'Example' => App\Facades\Example::class,
213 188
     ])->toArray(),
214 189
 
215 190
 ];

+ 1
- 1
config/auth.php 查看文件

@@ -80,7 +80,7 @@ return [
80 80
     | than one user table or model in the application and you want to have
81 81
     | separate password reset settings based on the specific user types.
82 82
     |
83
-    | The expire time is the number of minutes that each reset token will be
83
+    | The expiry time is the number of minutes that each reset token will be
84 84
     | considered valid. This security feature keeps tokens short-lived so
85 85
     | they have less time to be guessed. You may change this as needed.
86 86
     |

+ 122
- 0
config/filament-companies.php 查看文件

@@ -0,0 +1,122 @@
1
+<?php
2
+
3
+use Wallo\FilamentCompanies\Features;
4
+use Wallo\FilamentCompanies\Http\Middleware\AuthenticateSession;
5
+use Wallo\FilamentCompanies\Providers;
6
+
7
+return [
8
+
9
+    /*
10
+    |--------------------------------------------------------------------------
11
+    | Company Stack
12
+    |--------------------------------------------------------------------------
13
+    |
14
+    | This configuration value informs Company which "stack" you will be
15
+    | using for your application. In general, this value is set for you
16
+    | during installation and will not need to be changed after that.
17
+    |
18
+    */
19
+
20
+    'stack' => 'filament',
21
+
22
+    /*
23
+     |--------------------------------------------------------------------------
24
+     | Company Route Middleware
25
+     |--------------------------------------------------------------------------
26
+     |
27
+     | Here you may specify which middleware Company will assign to the routes
28
+     | that it registers with the application. When necessary, you may modify
29
+     | these middleware; however, this default value is usually sufficient.
30
+     |
31
+     */
32
+
33
+    'middleware' => config('filament.middleware.base'),
34
+
35
+    'auth_session' => AuthenticateSession::class,
36
+
37
+    /*
38
+    |--------------------------------------------------------------------------
39
+    | Company Guard
40
+    |--------------------------------------------------------------------------
41
+    |
42
+    | Here you may specify the authentication guard Company will use while
43
+    | authenticating users. This value should correspond with one of your
44
+    | guards that is already present in your "auth" configuration file.
45
+    |
46
+    */
47
+
48
+    'guard' => 'sanctum',
49
+
50
+    /*
51
+    |--------------------------------------------------------------------------
52
+    | Socialite Providers
53
+    |--------------------------------------------------------------------------
54
+    |
55
+    | Here you may specify the providers your application supports for OAuth.
56
+    | Out of the box, FilamentCompanies provides support for all the OAuth
57
+    | providers that are supported by Laravel Socialite.
58
+    |
59
+    */
60
+
61
+    'providers' => [
62
+        Providers::github(),
63
+    ],
64
+
65
+    /*
66
+    |--------------------------------------------------------------------------
67
+    | Features
68
+    |--------------------------------------------------------------------------
69
+    |
70
+    | Some of Company's features are optional. You may disable the features
71
+    | by removing them from this array. You're free to only remove some of
72
+    | these features, or you can even remove all of these if you need to.
73
+    |
74
+    */
75
+
76
+    'features' => [
77
+        Features::termsAndPrivacyPolicy(),
78
+        Features::profilePhotos(),
79
+        Features::api(),
80
+        Features::companies(['invitations' => true]),
81
+        Features::accountDeletion(),
82
+        Features::socialite(['rememberSession' => true, 'providerAvatars' => true]),
83
+    ],
84
+
85
+    /*
86
+    |--------------------------------------------------------------------------
87
+    | Layout
88
+    |--------------------------------------------------------------------------
89
+    |
90
+    | This is the configuration for the general layout of the package.
91
+    |
92
+    | Supported:
93
+    | "sm", "md", "lg", "xl", "2xl",
94
+    | "3xl", "4xl", "5xl", "6xl", "7xl", "full"
95
+    |
96
+    */
97
+
98
+    'layout' => [
99
+        'modals' => [
100
+            'dialog_modal_width' => '2xl',
101
+            'api_tokens' => [
102
+                'create_modal_width' => '2xl',
103
+                'edit_modal_width' => '2xl',
104
+                'revoke_modal_width' => 'md',
105
+            ],
106
+        ],
107
+    ],
108
+
109
+    /*
110
+    |--------------------------------------------------------------------------
111
+    | Profile Photo Disk
112
+    |--------------------------------------------------------------------------
113
+    |
114
+    | This configuration value determines the default disk that will be used
115
+    | when storing profile photos for your application's users. Typically,
116
+    | this will be the "public" disk, but you may adjust this if needed.
117
+    |
118
+    */
119
+
120
+    'profile_photo_disk' => 'public',
121
+
122
+];

+ 334
- 0
config/filament.php 查看文件

@@ -0,0 +1,334 @@
1
+<?php
2
+
3
+use App\Http\Middleware\Authenticate;
4
+use Filament\Http\Middleware\DispatchServingFilamentEvent;
5
+use Filament\Http\Middleware\MirrorConfigToSubpackages;
6
+use Filament\Pages;
7
+use Filament\Widgets;
8
+use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
9
+use Illuminate\Cookie\Middleware\EncryptCookies;
10
+use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
11
+use Illuminate\Routing\Middleware\SubstituteBindings;
12
+use Wallo\FilamentCompanies\Http\Middleware\AuthenticateSession;
13
+use Illuminate\Session\Middleware\StartSession;
14
+use Illuminate\View\Middleware\ShareErrorsFromSession;
15
+
16
+return [
17
+
18
+    /*
19
+    |--------------------------------------------------------------------------
20
+    | Filament Path
21
+    |--------------------------------------------------------------------------
22
+    |
23
+    | The default is `admin` but you can change it to whatever works best and
24
+    | doesn't conflict with the routing in your application.
25
+    |
26
+    */
27
+
28
+    'path' => env('FILAMENT_PATH', 'admin'),
29
+
30
+    /*
31
+    |--------------------------------------------------------------------------
32
+    | Filament Core Path
33
+    |--------------------------------------------------------------------------
34
+    |
35
+    | This is the path which Filament will use to load its core routes and assets.
36
+    | You may change it if it conflicts with your other routes.
37
+    |
38
+    */
39
+
40
+    'core_path' => env('FILAMENT_CORE_PATH', 'filament'),
41
+
42
+    /*
43
+    |--------------------------------------------------------------------------
44
+    | Filament Domain
45
+    |--------------------------------------------------------------------------
46
+    |
47
+    | You may change the domain where Filament should be active. If the domain
48
+    | is empty, all domains will be valid.
49
+    |
50
+    */
51
+
52
+    'domain' => env('FILAMENT_DOMAIN'),
53
+
54
+    /*
55
+    |--------------------------------------------------------------------------
56
+    | Homepage URL
57
+    |--------------------------------------------------------------------------
58
+    |
59
+    | This is the URL that Filament will redirect the user to when they click
60
+    | on the sidebar's header.
61
+    |
62
+    */
63
+
64
+    'home_url' => '/',
65
+
66
+    /*
67
+    |--------------------------------------------------------------------------
68
+    | Brand Name
69
+    |--------------------------------------------------------------------------
70
+    |
71
+    | This will be displayed on the login page and in the sidebar's header.
72
+    |
73
+    */
74
+
75
+    'brand' => env('APP_NAME'),
76
+
77
+    /*
78
+    |--------------------------------------------------------------------------
79
+    | Auth
80
+    |--------------------------------------------------------------------------
81
+    |
82
+    | This is the configuration that Filament will use to handle authentication
83
+    | into the admin panel.
84
+    |
85
+    */
86
+
87
+    'auth' => [
88
+        'guard' => env('FILAMENT_AUTH_GUARD', 'web'),
89
+        'pages' => [
90
+            'login' => null,
91
+        ],
92
+    ],
93
+
94
+    /*
95
+    |--------------------------------------------------------------------------
96
+    | Pages
97
+    |--------------------------------------------------------------------------
98
+    |
99
+    | This is the namespace and directory that Filament will automatically
100
+    | register pages from. You may also register pages here.
101
+    |
102
+    */
103
+
104
+    'pages' => [
105
+        'namespace' => 'App\\Filament\\Pages',
106
+        'path' => app_path('Filament/Pages'),
107
+        'register' => [
108
+            //
109
+        ],
110
+    ],
111
+
112
+    /*
113
+    |--------------------------------------------------------------------------
114
+    | Resources
115
+    |--------------------------------------------------------------------------
116
+    |
117
+    | This is the namespace and directory that Filament will automatically
118
+    | register resources from. You may also register resources here.
119
+    |
120
+    */
121
+
122
+    'resources' => [
123
+        'namespace' => 'App\\Filament\\Resources',
124
+        'path' => app_path('Filament/Resources'),
125
+        'register' => [],
126
+    ],
127
+
128
+    /*
129
+    |--------------------------------------------------------------------------
130
+    | Widgets
131
+    |--------------------------------------------------------------------------
132
+    |
133
+    | This is the namespace and directory that Filament will automatically
134
+    | register dashboard widgets from. You may also register widgets here.
135
+    |
136
+    */
137
+
138
+    'widgets' => [
139
+        'namespace' => 'App\\Filament\\Widgets',
140
+        'path' => app_path('Filament/Widgets'),
141
+        'register' => [
142
+            Widgets\AccountWidget::class,
143
+            Widgets\FilamentInfoWidget::class,
144
+        ],
145
+    ],
146
+
147
+    /*
148
+    |--------------------------------------------------------------------------
149
+    | Livewire
150
+    |--------------------------------------------------------------------------
151
+    |
152
+    | This is the namespace and directory that Filament will automatically
153
+    | register Livewire components inside.
154
+    |
155
+    */
156
+
157
+    'livewire' => [
158
+        'namespace' => 'App\\Filament',
159
+        'path' => app_path('Filament'),
160
+    ],
161
+
162
+    /*
163
+    |--------------------------------------------------------------------------
164
+    | Dark mode
165
+    |--------------------------------------------------------------------------
166
+    |
167
+    | By enabling this feature, your users are able to select between a light
168
+    | and dark appearance for the admin panel, or let their system decide.
169
+    |
170
+    */
171
+
172
+    'dark_mode' => true,
173
+
174
+    /*
175
+    |--------------------------------------------------------------------------
176
+    | Database notifications
177
+    |--------------------------------------------------------------------------
178
+    |
179
+    | By enabling this feature, your users are able to open a slide-over within
180
+    | the admin panel to view their database notifications.
181
+    |
182
+    */
183
+
184
+    'database_notifications' => [
185
+        'enabled' => false,
186
+        'polling_interval' => '30s',
187
+    ],
188
+
189
+    /*
190
+    |--------------------------------------------------------------------------
191
+    | Broadcasting
192
+    |--------------------------------------------------------------------------
193
+    |
194
+    | By uncommenting the Laravel Echo configuration, you may connect your
195
+    | admin panel to any Pusher-compatible websockets server.
196
+    |
197
+    | This will allow your admin panel to receive real-time notifications.
198
+    |
199
+    */
200
+
201
+    'broadcasting' => [
202
+
203
+        // 'echo' => [
204
+        //     'broadcaster' => 'pusher',
205
+        //     'key' => env('VITE_PUSHER_APP_KEY'),
206
+        //     'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
207
+        //     'forceTLS' => true,
208
+        // ],
209
+
210
+    ],
211
+
212
+    /*
213
+    |--------------------------------------------------------------------------
214
+    | Layout
215
+    |--------------------------------------------------------------------------
216
+    |
217
+    | This is the configuration for the general layout of the admin panel.
218
+    |
219
+    | You may configure the max content width from `xl` to `7xl`, or `full`
220
+    | for no max width.
221
+    |
222
+    */
223
+
224
+    'layout' => [
225
+        'actions' => [
226
+            'modal' => [
227
+                'actions' => [
228
+                    'alignment' => 'left',
229
+                ],
230
+            ],
231
+        ],
232
+        'forms' => [
233
+            'actions' => [
234
+                'alignment' => 'left',
235
+                'are_sticky' => false,
236
+            ],
237
+            'have_inline_labels' => false,
238
+        ],
239
+        'footer' => [
240
+            'should_show_logo' => false,
241
+        ],
242
+        'max_content_width' => null,
243
+        'notifications' => [
244
+            'vertical_alignment' => 'top',
245
+            'alignment' => 'center',
246
+        ],
247
+        'sidebar' => [
248
+            'is_collapsible_on_desktop' => true,
249
+            'groups' => [
250
+                'are_collapsible' => true,
251
+            ],
252
+            'width' => null,
253
+            'collapsed_width' => null,
254
+        ],
255
+    ],
256
+
257
+    /*
258
+    |--------------------------------------------------------------------------
259
+    | Favicon
260
+    |--------------------------------------------------------------------------
261
+    |
262
+    | This is the path to the favicon used for pages in the admin panel.
263
+    |
264
+    */
265
+
266
+    'favicon' => null,
267
+
268
+    /*
269
+    |--------------------------------------------------------------------------
270
+    | Default Avatar Provider
271
+    |--------------------------------------------------------------------------
272
+    |
273
+    | This is the service that will be used to retrieve default avatars if one
274
+    | has not been uploaded.
275
+    |
276
+    */
277
+
278
+    'default_avatar_provider' => \Filament\AvatarProviders\UiAvatarsProvider::class,
279
+
280
+    /*
281
+    |--------------------------------------------------------------------------
282
+    | Default Filesystem Disk
283
+    |--------------------------------------------------------------------------
284
+    |
285
+    | This is the storage disk Filament will use to put media. You may use any
286
+    | of the disks defined in the `config/filesystems.php`.
287
+    |
288
+    */
289
+
290
+    'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DRIVER', 'public'),
291
+
292
+    /*
293
+    |--------------------------------------------------------------------------
294
+    | Google Fonts
295
+    |--------------------------------------------------------------------------
296
+    |
297
+    | This is the URL for Google Fonts that should be loaded. You may use any
298
+    | font, or set to `null` to prevent any Google Fonts from loading.
299
+    |
300
+    | When using a custom font, you should also set the font family in your
301
+    | custom theme's `tailwind.config.js` file.
302
+    |
303
+    */
304
+
305
+    'google_fonts' => 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap',
306
+
307
+    /*
308
+    |--------------------------------------------------------------------------
309
+    | Middleware
310
+    |--------------------------------------------------------------------------
311
+    |
312
+    | You may customize the middleware stack that Filament uses to handle
313
+    | requests.
314
+    |
315
+    */
316
+
317
+    'middleware' => [
318
+        'auth' => [
319
+            Authenticate::class,
320
+        ],
321
+        'base' => [
322
+            EncryptCookies::class,
323
+            AddQueuedCookiesToResponse::class,
324
+            StartSession::class,
325
+            AuthenticateSession::class,
326
+            ShareErrorsFromSession::class,
327
+            VerifyCsrfToken::class,
328
+            SubstituteBindings::class,
329
+            DispatchServingFilamentEvent::class,
330
+            MirrorConfigToSubpackages::class,
331
+        ],
332
+    ],
333
+
334
+];

+ 68
- 0
config/forms.php 查看文件

@@ -0,0 +1,68 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Components
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | These are the settings that Filament will use to control the appearance
11
+    | and behaviour of form components.
12
+    |
13
+    */
14
+
15
+    'components' => [
16
+
17
+        'actions' => [
18
+
19
+            'modal' => [
20
+
21
+                'actions' => [
22
+                    'alignment' => 'left',
23
+                ],
24
+
25
+            ],
26
+
27
+        ],
28
+
29
+        'date_time_picker' => [
30
+            'first_day_of_week' => 1, // 0 to 7 are accepted values, with Monday as 1 and Sunday as 7 or 0.
31
+            'display_formats' => [
32
+                'date' => 'M j, Y',
33
+                'date_time' => 'M j, Y H:i',
34
+                'date_time_with_seconds' => 'M j, Y H:i:s',
35
+                'time' => 'H:i',
36
+                'time_with_seconds' => 'H:i:s',
37
+            ],
38
+        ],
39
+
40
+    ],
41
+
42
+    /*
43
+    |--------------------------------------------------------------------------
44
+    | Default Filesystem Disk
45
+    |--------------------------------------------------------------------------
46
+    |
47
+    | This is the storage disk Filament will use to put media. You may use any
48
+    | of the disks defined in the `config/filesystems.php`.
49
+    |
50
+    */
51
+
52
+    'default_filesystem_disk' => env('FORMS_FILESYSTEM_DRIVER', 'public'),
53
+
54
+    /*
55
+    |--------------------------------------------------------------------------
56
+    | Dark mode
57
+    |--------------------------------------------------------------------------
58
+    |
59
+    | By enabling this setting, your forms will be ready for Tailwind's Dark
60
+    | Mode feature.
61
+    |
62
+    | https://tailwindcss.com/docs/dark-mode
63
+    |
64
+    */
65
+
66
+    'dark_mode' => false,
67
+
68
+];

+ 147
- 0
config/fortify.php 查看文件

@@ -0,0 +1,147 @@
1
+<?php
2
+
3
+use App\Providers\RouteServiceProvider;
4
+use Laravel\Fortify\Features;
5
+
6
+return [
7
+
8
+    /*
9
+    |--------------------------------------------------------------------------
10
+    | Fortify Guard
11
+    |--------------------------------------------------------------------------
12
+    |
13
+    | Here you may specify which authentication guard Fortify will use while
14
+    | authenticating users. This value should correspond with one of your
15
+    | guards that is already present in your "auth" configuration file.
16
+    |
17
+    */
18
+
19
+    'guard' => 'web',
20
+
21
+    /*
22
+    |--------------------------------------------------------------------------
23
+    | Fortify Password Broker
24
+    |--------------------------------------------------------------------------
25
+    |
26
+    | Here you may specify which password broker Fortify can use when a user
27
+    | is resetting their password. This configured value should match one
28
+    | of your password brokers setup in your "auth" configuration file.
29
+    |
30
+    */
31
+
32
+    'passwords' => 'users',
33
+
34
+    /*
35
+    |--------------------------------------------------------------------------
36
+    | Username / Email
37
+    |--------------------------------------------------------------------------
38
+    |
39
+    | This value defines which model attribute should be considered as your
40
+    | application's "username" field. Typically, this might be the email
41
+    | address of the users but you are free to change this value here.
42
+    |
43
+    | Out of the box, Fortify expects forgot password and reset password
44
+    | requests to have a field named 'email'. If the application uses
45
+    | another name for the field you may define it below as needed.
46
+    |
47
+    */
48
+
49
+    'username' => 'email',
50
+
51
+    'email' => 'email',
52
+
53
+    /*
54
+    |--------------------------------------------------------------------------
55
+    | Home Path
56
+    |--------------------------------------------------------------------------
57
+    |
58
+    | Here you may configure the path where users will get redirected during
59
+    | authentication or password reset when the operations are successful
60
+    | and the user is authenticated. You are free to change this value.
61
+    |
62
+    */
63
+
64
+    'home' => config('filament.path'),
65
+
66
+    /*
67
+    |--------------------------------------------------------------------------
68
+    | Fortify Routes Prefix / Subdomain
69
+    |--------------------------------------------------------------------------
70
+    |
71
+    | Here you may specify which prefix Fortify will assign to all the routes
72
+    | that it registers with the application. If necessary, you may change
73
+    | subdomain under which all of the Fortify routes will be available.
74
+    |
75
+    */
76
+
77
+    'prefix' => '',
78
+
79
+    'domain' => null,
80
+
81
+    /*
82
+    |--------------------------------------------------------------------------
83
+    | Fortify Routes Middleware
84
+    |--------------------------------------------------------------------------
85
+    |
86
+    | Here you may specify which middleware Fortify will assign to the routes
87
+    | that it registers with the application. If necessary, you may change
88
+    | these middleware but typically this provided default is preferred.
89
+    |
90
+    */
91
+
92
+    'middleware' => config('filament.middleware.base'),
93
+
94
+    /*
95
+    |--------------------------------------------------------------------------
96
+    | Rate Limiting
97
+    |--------------------------------------------------------------------------
98
+    |
99
+    | By default, Fortify will throttle logins to five requests per minute for
100
+    | every email and IP address combination. However, if you would like to
101
+    | specify a custom rate limiter to call then you may specify it here.
102
+    |
103
+    */
104
+
105
+    'limiters' => [
106
+        'login' => 'login',
107
+        'two-factor' => 'two-factor',
108
+    ],
109
+
110
+    /*
111
+    |--------------------------------------------------------------------------
112
+    | Register View Routes
113
+    |--------------------------------------------------------------------------
114
+    |
115
+    | Here you may specify if the routes returning views should be disabled as
116
+    | you may not need them when building your own application. This may be
117
+    | especially true if you're writing a custom single-page application.
118
+    |
119
+    */
120
+
121
+    'views' => true,
122
+
123
+    /*
124
+    |--------------------------------------------------------------------------
125
+    | Features
126
+    |--------------------------------------------------------------------------
127
+    |
128
+    | Some of the Fortify features are optional. You may disable the features
129
+    | by removing them from this array. You're free to only remove some of
130
+    | these features or you can even remove all of these if you need to.
131
+    |
132
+    */
133
+
134
+    'features' => [
135
+        Features::registration(),
136
+        Features::resetPasswords(),
137
+        // Features::emailVerification(),
138
+        Features::updateProfileInformation(),
139
+        Features::updatePasswords(),
140
+        Features::twoFactorAuthentication([
141
+            'confirm' => true,
142
+            'confirmPassword' => true,
143
+            // 'window' => 0,
144
+        ]),
145
+    ],
146
+
147
+];

+ 9
- 0
config/logging.php 查看文件

@@ -3,6 +3,7 @@
3 3
 use Monolog\Handler\NullHandler;
4 4
 use Monolog\Handler\StreamHandler;
5 5
 use Monolog\Handler\SyslogUdpHandler;
6
+use Monolog\Processor\PsrLogMessageProcessor;
6 7
 
7 8
 return [
8 9
 
@@ -61,6 +62,7 @@ return [
61 62
             'driver' => 'single',
62 63
             'path' => storage_path('logs/laravel.log'),
63 64
             'level' => env('LOG_LEVEL', 'debug'),
65
+            'replace_placeholders' => true,
64 66
         ],
65 67
 
66 68
         'daily' => [
@@ -68,6 +70,7 @@ return [
68 70
             'path' => storage_path('logs/laravel.log'),
69 71
             'level' => env('LOG_LEVEL', 'debug'),
70 72
             'days' => 14,
73
+            'replace_placeholders' => true,
71 74
         ],
72 75
 
73 76
         'slack' => [
@@ -76,6 +79,7 @@ return [
76 79
             'username' => 'Laravel Log',
77 80
             'emoji' => ':boom:',
78 81
             'level' => env('LOG_LEVEL', 'critical'),
82
+            'replace_placeholders' => true,
79 83
         ],
80 84
 
81 85
         'papertrail' => [
@@ -87,6 +91,7 @@ return [
87 91
                 'port' => env('PAPERTRAIL_PORT'),
88 92
                 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
89 93
             ],
94
+            'processors' => [PsrLogMessageProcessor::class],
90 95
         ],
91 96
 
92 97
         'stderr' => [
@@ -97,16 +102,20 @@ return [
97 102
             'with' => [
98 103
                 'stream' => 'php://stderr',
99 104
             ],
105
+            'processors' => [PsrLogMessageProcessor::class],
100 106
         ],
101 107
 
102 108
         'syslog' => [
103 109
             'driver' => 'syslog',
104 110
             'level' => env('LOG_LEVEL', 'debug'),
111
+            'facility' => LOG_USER,
112
+            'replace_placeholders' => true,
105 113
         ],
106 114
 
107 115
         'errorlog' => [
108 116
             'driver' => 'errorlog',
109 117
             'level' => env('LOG_LEVEL', 'debug'),
118
+            'replace_placeholders' => true,
110 119
         ],
111 120
 
112 121
         'null' => [

+ 51
- 0
config/notifications.php 查看文件

@@ -0,0 +1,51 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Dark mode
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | By enabling this setting, your notifications will be ready for Tailwind's
11
+    | Dark Mode feature.
12
+    |
13
+    | https://tailwindcss.com/docs/dark-mode
14
+    |
15
+    */
16
+
17
+    'dark_mode' => false,
18
+
19
+    /*
20
+    |--------------------------------------------------------------------------
21
+    | Database notifications
22
+    |--------------------------------------------------------------------------
23
+    |
24
+    | By enabling this feature, your users are able to open a slide-over within
25
+    | the app to view their database notifications.
26
+    |
27
+    */
28
+
29
+    'database' => [
30
+        'enabled' => false,
31
+        'trigger' => null,
32
+        'polling_interval' => '30s',
33
+    ],
34
+
35
+    /*
36
+    |--------------------------------------------------------------------------
37
+    | Layout
38
+    |--------------------------------------------------------------------------
39
+    |
40
+    | This is the configuration for the general layout of notifications.
41
+    |
42
+    */
43
+
44
+    'layout' => [
45
+        'alignment' => [
46
+            'horizontal' => 'right',
47
+            'vertical' => 'top',
48
+        ],
49
+    ],
50
+
51
+];

+ 16
- 0
config/queue.php 查看文件

@@ -73,6 +73,22 @@ return [
73 73
 
74 74
     ],
75 75
 
76
+    /*
77
+    |--------------------------------------------------------------------------
78
+    | Job Batching
79
+    |--------------------------------------------------------------------------
80
+    |
81
+    | The following options configure the database and table that store job
82
+    | batching information. These options can be updated to any database
83
+    | connection and table which has been defined by your application.
84
+    |
85
+    */
86
+
87
+    'batching' => [
88
+        'database' => env('DB_CONNECTION', 'mysql'),
89
+        'table' => 'job_batches',
90
+    ],
91
+
76 92
     /*
77 93
     |--------------------------------------------------------------------------
78 94
     | Failed Queue Jobs

+ 2
- 2
config/sanctum.php 查看文件

@@ -41,8 +41,8 @@ return [
41 41
     |--------------------------------------------------------------------------
42 42
     |
43 43
     | This value controls the number of minutes until an issued token will be
44
-    | considered expired. If this value is null, personal access tokens do
45
-    | not expire. This won't tweak the lifetime of first-party sessions.
44
+    | considered expired. This will override any values set in the token's
45
+    | "expires_at" attribute, but first-party sessions are not affected.
46 46
     |
47 47
     */
48 48
 

+ 6
- 0
config/services.php 查看文件

@@ -31,4 +31,10 @@ return [
31 31
         'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
32 32
     ],
33 33
 
34
+    'github' => [
35
+        'client_id' => env('GITHUB_CLIENT_ID'),
36
+        'client_secret' => env('GITHUB_CLIENT_SECRET'),
37
+        'redirect' => 'https://erpsaas.test/oauth/github/callback',
38
+    ],
39
+
34 40
 ];

+ 1
- 1
config/session.php 查看文件

@@ -18,7 +18,7 @@ return [
18 18
     |
19 19
     */
20 20
 
21
-    'driver' => env('SESSION_DRIVER', 'file'),
21
+    'driver' => env('SESSION_DRIVER', 'database'),
22 22
 
23 23
     /*
24 24
     |--------------------------------------------------------------------------

+ 81
- 0
config/tables.php 查看文件

@@ -0,0 +1,81 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Date / Time Formatting
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | These are the formats that Filament will use to display dates and times
11
+    | by default.
12
+    |
13
+    */
14
+
15
+    'date_format' => 'M j, Y',
16
+    'date_time_format' => 'M j, Y H:i:s',
17
+    'time_format' => 'H:i:s',
18
+
19
+    /*
20
+    |--------------------------------------------------------------------------
21
+    | Default Filesystem Disk
22
+    |--------------------------------------------------------------------------
23
+    |
24
+    | This is the storage disk Filament will use to find media. You may use any
25
+    | of the disks defined in the `config/filesystems.php`.
26
+    |
27
+    */
28
+
29
+    'default_filesystem_disk' => env('TABLES_FILESYSTEM_DRIVER', 'public'),
30
+
31
+    /*
32
+    |--------------------------------------------------------------------------
33
+    | Dark mode
34
+    |--------------------------------------------------------------------------
35
+    |
36
+    | By enabling this setting, your tables will be ready for Tailwind's Dark
37
+    | Mode feature.
38
+    |
39
+    | https://tailwindcss.com/docs/dark-mode
40
+    |
41
+    */
42
+
43
+    'dark_mode' => false,
44
+
45
+    /*
46
+    |--------------------------------------------------------------------------
47
+    | Pagination
48
+    |--------------------------------------------------------------------------
49
+    |
50
+    | This is the configuration for the pagination of tables.
51
+    |
52
+    */
53
+
54
+    'pagination' => [
55
+        'default_records_per_page' => 10,
56
+        'records_per_page_select_options' => [5, 10, 25, 50, -1],
57
+    ],
58
+
59
+    /*
60
+    |--------------------------------------------------------------------------
61
+    | Layout
62
+    |--------------------------------------------------------------------------
63
+    |
64
+    | This is the configuration for the general layout of tables.
65
+    |
66
+    */
67
+
68
+    'layout' => [
69
+        'actions' => [
70
+            'cell' => [
71
+                'alignment' => 'right',
72
+            ],
73
+            'modal' => [
74
+                'actions' => [
75
+                    'alignment' => 'left',
76
+                ],
77
+            ],
78
+        ],
79
+    ],
80
+
81
+];

+ 31
- 0
database/factories/CompanyFactory.php 查看文件

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+namespace Database\Factories;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Database\Eloquent\Factories\Factory;
8
+
9
+class CompanyFactory extends Factory
10
+{
11
+    /**
12
+     * The name of the factory's corresponding model.
13
+     *
14
+     * @var string
15
+     */
16
+    protected $model = Company::class;
17
+
18
+    /**
19
+     * Define the model's default state.
20
+     *
21
+     * @return array<string, mixed>
22
+     */
23
+    public function definition(): array
24
+    {
25
+        return [
26
+            'name' => $this->faker->unique()->company(),
27
+            'user_id' => User::factory(),
28
+            'personal_company' => true,
29
+        ];
30
+    }
31
+}

+ 39
- 8
database/factories/UserFactory.php 查看文件

@@ -2,14 +2,21 @@
2 2
 
3 3
 namespace Database\Factories;
4 4
 
5
+use App\Models\Company;
6
+use App\Models\User;
5 7
 use Illuminate\Database\Eloquent\Factories\Factory;
6 8
 use Illuminate\Support\Str;
9
+use Wallo\FilamentCompanies\Features;
7 10
 
8
-/**
9
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
10
- */
11 11
 class UserFactory extends Factory
12 12
 {
13
+    /**
14
+     * The name of the factory's corresponding model.
15
+     *
16
+     * @var string
17
+     */
18
+    protected $model = User::class;
19
+
13 20
     /**
14 21
      * Define the model's default state.
15 22
      *
@@ -18,11 +25,15 @@ class UserFactory extends Factory
18 25
     public function definition(): array
19 26
     {
20 27
         return [
21
-            'name' => fake()->name(),
22
-            'email' => fake()->unique()->safeEmail(),
28
+            'name' => $this->faker->name(),
29
+            'email' => $this->faker->unique()->safeEmail(),
23 30
             'email_verified_at' => now(),
24 31
             'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
32
+            'two_factor_secret' => null,
33
+            'two_factor_recovery_codes' => null,
25 34
             'remember_token' => Str::random(10),
35
+            'profile_photo_path' => null,
36
+            'current_company_id' => null,
26 37
         ];
27 38
     }
28 39
 
@@ -31,8 +42,28 @@ class UserFactory extends Factory
31 42
      */
32 43
     public function unverified(): static
33 44
     {
34
-        return $this->state(fn (array $attributes) => [
35
-            'email_verified_at' => null,
36
-        ]);
45
+        return $this->state(function (array $attributes) {
46
+            return [
47
+                'email_verified_at' => null,
48
+            ];
49
+        });
50
+    }
51
+
52
+    /**
53
+     * Indicate that the user should have a personal company.
54
+     */
55
+    public function withPersonalCompany(): static
56
+    {
57
+        if (! Features::hasCompanyFeatures()) {
58
+            return $this->state([]);
59
+        }
60
+
61
+        return $this->has(
62
+            Company::factory()
63
+                ->state(function (array $attributes, User $user) {
64
+                    return ['name' => $user->name.'\'s Company', 'user_id' => $user->id, 'personal_company' => true];
65
+                }),
66
+            'ownedCompanies'
67
+        );
37 68
     }
38 69
 }

+ 7
- 1
database/migrations/2014_10_12_000000_create_users_table.php 查看文件

@@ -3,6 +3,7 @@
3 3
 use Illuminate\Database\Migrations\Migration;
4 4
 use Illuminate\Database\Schema\Blueprint;
5 5
 use Illuminate\Support\Facades\Schema;
6
+use Wallo\FilamentCompanies\Socialite;
6 7
 
7 8
 return new class extends Migration
8 9
 {
@@ -16,8 +17,13 @@ return new class extends Migration
16 17
             $table->string('name');
17 18
             $table->string('email')->unique();
18 19
             $table->timestamp('email_verified_at')->nullable();
19
-            $table->string('password');
20
+            $table->string('password')->nullable(
21
+                Socialite::hasSocialiteFeatures()
22
+            );
20 23
             $table->rememberToken();
24
+            $table->foreignId('current_company_id')->nullable();
25
+            $table->foreignId('current_connected_account_id')->nullable();
26
+            $table->string('profile_photo_path')->nullable();
21 27
             $table->timestamps();
22 28
         });
23 29
     }

+ 46
- 0
database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php 查看文件

@@ -0,0 +1,46 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+use Laravel\Fortify\Fortify;
7
+
8
+return new class extends Migration
9
+{
10
+    /**
11
+     * Run the migrations.
12
+     */
13
+    public function up(): void
14
+    {
15
+        Schema::table('users', function (Blueprint $table) {
16
+            $table->text('two_factor_secret')
17
+                    ->after('password')
18
+                    ->nullable();
19
+
20
+            $table->text('two_factor_recovery_codes')
21
+                    ->after('two_factor_secret')
22
+                    ->nullable();
23
+
24
+            if (Fortify::confirmsTwoFactorAuthentication()) {
25
+                $table->timestamp('two_factor_confirmed_at')
26
+                        ->after('two_factor_recovery_codes')
27
+                        ->nullable();
28
+            }
29
+        });
30
+    }
31
+
32
+    /**
33
+     * Reverse the migrations.
34
+     */
35
+    public function down(): void
36
+    {
37
+        Schema::table('users', function (Blueprint $table) {
38
+            $table->dropColumn(array_merge([
39
+                'two_factor_secret',
40
+                'two_factor_recovery_codes',
41
+            ], Fortify::confirmsTwoFactorAuthentication() ? [
42
+                'two_factor_confirmed_at',
43
+            ] : []));
44
+        });
45
+    }
46
+};

+ 30
- 0
database/migrations/2020_05_21_100000_create_companies_table.php 查看文件

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('companies', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('user_id')->index();
17
+            $table->string('name');
18
+            $table->boolean('personal_company');
19
+            $table->timestamps();
20
+        });
21
+    }
22
+
23
+    /**
24
+     * Reverse the migrations.
25
+     */
26
+    public function down(): void
27
+    {
28
+        Schema::dropIfExists('companies');
29
+    }
30
+};

+ 32
- 0
database/migrations/2020_05_21_200000_create_company_user_table.php 查看文件

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('company_user', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('company_id');
17
+            $table->foreignId('user_id');
18
+            $table->string('role')->nullable();
19
+            $table->timestamps();
20
+
21
+            $table->unique(['company_id', 'user_id']);
22
+        });
23
+    }
24
+
25
+    /**
26
+     * Reverse the migrations.
27
+     */
28
+    public function down(): void
29
+    {
30
+        Schema::dropIfExists('company_user');
31
+    }
32
+};

+ 32
- 0
database/migrations/2020_05_21_300000_create_company_invitations_table.php 查看文件

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('company_invitations', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
+            $table->string('email');
18
+            $table->string('role')->nullable();
19
+            $table->timestamps();
20
+
21
+            $table->unique(['company_id', 'email']);
22
+        });
23
+    }
24
+
25
+    /**
26
+     * Reverse the migrations.
27
+     */
28
+    public function down(): void
29
+    {
30
+        Schema::dropIfExists('company_invitations');
31
+    }
32
+};

+ 42
- 0
database/migrations/2020_12_22_000000_create_connected_accounts_table.php 查看文件

@@ -0,0 +1,42 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('connected_accounts', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('user_id');
17
+            $table->string('provider');
18
+            $table->string('provider_id');
19
+            $table->string('name')->nullable();
20
+            $table->string('nickname')->nullable();
21
+            $table->string('email')->nullable();
22
+            $table->string('telephone')->nullable();
23
+            $table->text('avatar_path')->nullable();
24
+            $table->string('token', 1000);
25
+            $table->string('secret')->nullable(); // OAuth1
26
+            $table->string('refresh_token', 1000)->nullable(); // OAuth2
27
+            $table->dateTime('expires_at')->nullable(); // OAuth2
28
+            $table->timestamps();
29
+
30
+            $table->index(['user_id', 'id']);
31
+            $table->index(['provider', 'provider_id']);
32
+        });
33
+    }
34
+
35
+    /**
36
+     * Reverse the migrations.
37
+     */
38
+    public function down(): void
39
+    {
40
+        Schema::dropIfExists('connected_accounts');
41
+    }
42
+};

+ 31
- 0
database/migrations/2023_05_01_034040_create_sessions_table.php 查看文件

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('sessions', function (Blueprint $table) {
15
+            $table->string('id')->primary();
16
+            $table->foreignId('user_id')->nullable()->index();
17
+            $table->string('ip_address', 45)->nullable();
18
+            $table->text('user_agent')->nullable();
19
+            $table->longText('payload');
20
+            $table->integer('last_activity')->index();
21
+        });
22
+    }
23
+
24
+    /**
25
+     * Reverse the migrations.
26
+     */
27
+    public function down(): void
28
+    {
29
+        Schema::dropIfExists('sessions');
30
+    }
31
+};

+ 3085
- 0
package-lock.json
文件差异内容过多而无法显示
查看文件


+ 9
- 0
package.json 查看文件

@@ -5,8 +5,17 @@
5 5
         "build": "vite build"
6 6
     },
7 7
     "devDependencies": {
8
+        "@alpinejs/focus": "^3.12.0",
9
+        "@awcodes/alpine-floating-ui": "^3.5.0",
10
+        "@tailwindcss/forms": "^0.5.3",
11
+        "@tailwindcss/typography": "^0.5.9",
12
+        "alpinejs": "^3.12.0",
13
+        "autoprefixer": "^10.4.14",
8 14
         "axios": "^1.1.2",
9 15
         "laravel-vite-plugin": "^0.7.2",
16
+        "postcss": "^8.4.23",
17
+        "tailwindcss": "^3.3.2",
18
+        "tippy.js": "^6.3.7",
10 19
         "vite": "^4.0.0"
11 20
     }
12 21
 }

+ 2
- 2
phpunit.xml 查看文件

@@ -12,11 +12,11 @@
12 12
             <directory suffix="Test.php">./tests/Feature</directory>
13 13
         </testsuite>
14 14
     </testsuites>
15
-    <coverage>
15
+    <source>
16 16
         <include>
17 17
             <directory suffix=".php">./app</directory>
18 18
         </include>
19
-    </coverage>
19
+    </source>
20 20
     <php>
21 21
         <env name="APP_ENV" value="testing"/>
22 22
         <env name="BCRYPT_ROUNDS" value="4"/>

+ 6
- 0
postcss.config.js 查看文件

@@ -0,0 +1,6 @@
1
+module.exports = {
2
+    plugins: {
3
+        tailwindcss: {},
4
+        autoprefixer: {},
5
+    },
6
+}

+ 5
- 0
resources/css/app.css 查看文件

@@ -0,0 +1,5 @@
1
+@import '../../vendor/filament/forms/dist/module.esm.css';
2
+
3
+@tailwind base;
4
+@tailwind components;
5
+@tailwind utilities;

+ 1
- 0
resources/css/filament.css 查看文件

@@ -0,0 +1 @@
1
+@import '../../vendor/filament/filament/resources/css/app.css';

+ 14
- 1
resources/js/app.js 查看文件

@@ -1 +1,14 @@
1
-import './bootstrap';
1
+import Alpine from 'alpinejs'
2
+import Focus from '@alpinejs/focus'
3
+import AlpineFloatingUI from '@awcodes/alpine-floating-ui'
4
+import FormsAlpinePlugin from '../../vendor/filament/forms/dist/module.esm'
5
+import NotificationsAlpinePlugin from '../../vendor/filament/notifications/dist/module.esm'
6
+
7
+Alpine.plugin(Focus)
8
+Alpine.plugin(AlpineFloatingUI)
9
+Alpine.plugin(FormsAlpinePlugin)
10
+Alpine.plugin(NotificationsAlpinePlugin)
11
+
12
+window.Alpine = Alpine
13
+
14
+Alpine.start()

+ 3
- 0
resources/markdown/policy.md 查看文件

@@ -0,0 +1,3 @@
1
+# Privacy Policy
2
+
3
+Edit this file to define the privacy policy for your application.

+ 3
- 0
resources/markdown/terms.md 查看文件

@@ -0,0 +1,3 @@
1
+# Terms of Service
2
+
3
+Edit this file to define the terms of service for your application.

+ 18
- 0
resources/views/filament/components/companies/avatar-column.blade.php 查看文件

@@ -0,0 +1,18 @@
1
+<div class="px-4 py-3">
2
+    <div class="flex items-center">
3
+        <div style="height: 40px; width: 40px;" class="overflow-hidden rounded-full">
4
+            <img src="{{ $getRecord()->owner->profile_photo_url }}" alt="{{ $getRecord()->owner->name }}" style="height: 40px; width: 40px;" class="object-cover object-center">
5
+        </div>
6
+        <div class="ml-4 font-semibold">
7
+            <div class="inline-flex items-center space-x-1 rtl:space-x-reverse">
8
+                <span>
9
+                    {{ $getRecord()->owner->name }}
10
+                </span>
11
+            </div>
12
+
13
+            <div class="text-sm text-gray-500">
14
+                <p><a href="mailto:{{ $getRecord()->owner->email }}">{{ $getRecord()->owner->email }}</a></p>
15
+            </div>
16
+        </div>
17
+    </div>
18
+</div>

+ 18
- 0
resources/views/filament/components/users/avatar-column.blade.php 查看文件

@@ -0,0 +1,18 @@
1
+<div class="px-4 py-3">
2
+    <div class="flex items-center">
3
+        <div style="height: 40px; width: 40px;" class="overflow-hidden rounded-full">
4
+            <img src="{{ $getRecord()->profile_photo_url }}" alt="{{ $getRecord()->name }}" style="height: 40px; width: 40px;" class="object-cover object-center">
5
+        </div>
6
+        <div class="ml-4 font-semibold">
7
+            <div class="inline-flex items-center space-x-1 rtl:space-x-reverse">
8
+                <span>
9
+                    {{ $getRecord()->name }}
10
+                </span>
11
+            </div>
12
+
13
+            <div class="text-sm text-gray-500">
14
+                <p><a href="mailto:{{ $getRecord()->email }}">{{ $getRecord()->email }}</a></p>
15
+            </div>
16
+        </div>
17
+    </div>
18
+</div>

+ 3
- 0
resources/views/filament/pages/companies.blade.php 查看文件

@@ -0,0 +1,3 @@
1
+<x-filament::page>
2
+
3
+</x-filament::page>

+ 3
- 0
resources/views/filament/pages/users.blade.php 查看文件

@@ -0,0 +1,3 @@
1
+<x-filament::page>
2
+
3
+</x-filament::page>

+ 24
- 0
resources/views/layouts/app.blade.php 查看文件

@@ -0,0 +1,24 @@
1
+<!DOCTYPE html>
2
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
3
+    <head>
4
+        <meta charset="utf-8">
5
+
6
+        <meta name="application-name" content="{{ config('app.name') }}">
7
+        <meta name="csrf-token" content="{{ csrf_token() }}">
8
+        <meta name="viewport" content="width=device-width, initial-scale=1">
9
+
10
+        <title>{{ config('app.name') }}</title>
11
+
12
+        <style>[x-cloak] { display: none !important; }</style>
13
+        @vite(['resources/css/app.css', 'resources/js/app.js'])
14
+        @livewireStyles
15
+        @livewireScripts
16
+        @stack('scripts')
17
+    </head>
18
+
19
+    <body class="antialiased">
20
+        {{ $slot }}
21
+
22
+        @livewire('notifications')
23
+    </body>
24
+</html>

+ 1
- 1
resources/views/welcome.blade.php 查看文件

@@ -20,7 +20,7 @@
20 20
             @if (Route::has('login'))
21 21
                 <div class="sm:fixed sm:top-0 sm:right-0 p-6 text-right">
22 22
                     @auth
23
-                        <a href="{{ url('/home') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Home</a>
23
+                        <a href="{{ url(config('filament.path')) }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">{{ucfirst(trans(config('filament.path')))}}</a>
24 24
                     @else
25 25
                         <a href="{{ route('login') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Log in</a>
26 26
 

+ 8
- 0
routes/web.php 查看文件

@@ -16,3 +16,11 @@ use Illuminate\Support\Facades\Route;
16 16
 Route::get('/', function () {
17 17
     return view('welcome');
18 18
 });
19
+
20
+Route::middleware([
21
+    'auth:sanctum',
22
+    config('filament-companies.auth_session'),
23
+    'verified'
24
+])->group(function () {
25
+
26
+});

+ 25
- 0
tailwind.config.js 查看文件

@@ -0,0 +1,25 @@
1
+const colors = require('tailwindcss/colors')
2
+
3
+/** @type {import('tailwindcss').Config} */
4
+module.exports = {
5
+  content: [
6
+      './resources/**/*.blade.php',
7
+      './vendor/filament/**/*.blade.php',
8
+  ],
9
+  darkMode: 'class',
10
+  theme: {
11
+    extend: {
12
+      colors: {
13
+        danger: colors.rose,
14
+        primary: colors.blue,
15
+        success: colors.green,
16
+        warning: colors.yellow,
17
+      },
18
+    },
19
+  },
20
+  plugins: [
21
+    require('@tailwindcss/forms'),
22
+    require('@tailwindcss/typography'),
23
+  ],
24
+}
25
+

+ 44
- 0
tests/Feature/AuthenticationTest.php 查看文件

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Tests\TestCase;
8
+
9
+class AuthenticationTest extends TestCase
10
+{
11
+    use RefreshDatabase;
12
+
13
+    public function test_login_screen_can_be_rendered(): void
14
+    {
15
+        $response = $this->get('/login');
16
+
17
+        $response->assertStatus(200);
18
+    }
19
+
20
+    public function test_users_can_authenticate_using_the_login_screen(): void
21
+    {
22
+        $user = User::factory()->create();
23
+
24
+        $response = $this->post('/login', [
25
+            'email' => $user->email,
26
+            'password' => 'password',
27
+        ]);
28
+
29
+        $this->assertAuthenticated();
30
+        $response->assertRedirect(config('filament.path'));
31
+    }
32
+
33
+    public function test_users_can_not_authenticate_with_invalid_password(): void
34
+    {
35
+        $user = User::factory()->create();
36
+
37
+        $this->post('/login', [
38
+            'email' => $user->email,
39
+            'password' => 'wrong-password',
40
+        ]);
41
+
42
+        $this->assertGuest();
43
+    }
44
+}

+ 24
- 0
tests/Feature/BrowserSessionsTest.php 查看文件

@@ -0,0 +1,24 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\LogoutOtherBrowserSessionsForm;
10
+
11
+class BrowserSessionsTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_other_browser_sessions_can_be_logged_out(): void
16
+    {
17
+        $this->actingAs($user = User::factory()->create());
18
+
19
+        Livewire::test(LogoutOtherBrowserSessionsForm::class)
20
+                ->set('password', 'password')
21
+                ->call('logoutOtherBrowserSessions')
22
+                ->assertSuccessful();
23
+    }
24
+}

+ 26
- 0
tests/Feature/CreateCompanyTest.php 查看文件

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\CreateCompanyForm;
10
+
11
+class CreateCompanyTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_companies_can_be_created(): void
16
+    {
17
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
18
+
19
+        Livewire::test(CreateCompanyForm::class)
20
+                    ->set(['state' => ['name' => 'Test Company']])
21
+                    ->call('createCompany');
22
+
23
+        $this->assertCount(2, $user->fresh()->ownedCompanies);
24
+        $this->assertEquals('Test Company', $user->fresh()->ownedCompanies()->latest('id')->first()->name);
25
+    }
26
+}

+ 50
- 0
tests/Feature/DeleteAccountTest.php 查看文件

@@ -0,0 +1,50 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Features;
10
+use Wallo\FilamentCompanies\Http\Livewire\DeleteUserForm;
11
+
12
+class DeleteAccountTest extends TestCase
13
+{
14
+    use RefreshDatabase;
15
+
16
+    public function test_user_accounts_can_be_deleted(): void
17
+    {
18
+        if (! Features::hasAccountDeletionFeatures()) {
19
+            $this->markTestSkipped('Account deletion is not enabled.');
20
+
21
+            return;
22
+        }
23
+
24
+        $this->actingAs($user = User::factory()->create());
25
+
26
+        $component = Livewire::test(DeleteUserForm::class)
27
+                        ->set('password', 'password')
28
+                        ->call('deleteUser');
29
+
30
+        $this->assertNull($user->fresh());
31
+    }
32
+
33
+    public function test_correct_password_must_be_provided_before_account_can_be_deleted(): void
34
+    {
35
+        if (! Features::hasAccountDeletionFeatures()) {
36
+            $this->markTestSkipped('Account deletion is not enabled.');
37
+
38
+            return;
39
+        }
40
+
41
+        $this->actingAs($user = User::factory()->create());
42
+
43
+        Livewire::test(DeleteUserForm::class)
44
+                        ->set('password', 'wrong-password')
45
+                        ->call('deleteUser')
46
+                        ->assertHasErrors(['password']);
47
+
48
+        $this->assertNotNull($user->fresh());
49
+    }
50
+}

+ 45
- 0
tests/Feature/DeleteCompanyTest.php 查看文件

@@ -0,0 +1,45 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Illuminate\Foundation\Testing\RefreshDatabase;
8
+use Livewire\Livewire;
9
+use Tests\TestCase;
10
+use Wallo\FilamentCompanies\Http\Livewire\DeleteCompanyForm;
11
+
12
+class DeleteCompanyTest extends TestCase
13
+{
14
+    use RefreshDatabase;
15
+
16
+    public function test_companies_can_be_deleted(): void
17
+    {
18
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
19
+
20
+        $user->ownedCompanies()->save($company = Company::factory()->make([
21
+            'personal_company' => false,
22
+        ]));
23
+
24
+        $company->users()->attach(
25
+            $otherUser = User::factory()->create(), ['role' => 'test-role']
26
+        );
27
+
28
+        $component = Livewire::test(DeleteCompanyForm::class, ['company' => $company->fresh()])
29
+                                ->call('deleteCompany');
30
+
31
+        $this->assertNull($company->fresh());
32
+        $this->assertCount(0, $otherUser->fresh()->companies);
33
+    }
34
+
35
+    public function test_personal_companies_cant_be_deleted(): void
36
+    {
37
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
38
+
39
+        $component = Livewire::test(DeleteCompanyForm::class, ['company' => $user->currentCompany])
40
+                                ->call('deleteCompany')
41
+                                ->assertHasErrors(['company']);
42
+
43
+        $this->assertNotNull($user->currentCompany->fresh());
44
+    }
45
+}

+ 78
- 0
tests/Feature/EmailVerificationTest.php 查看文件

@@ -0,0 +1,78 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Auth\Events\Verified;
7
+use Illuminate\Foundation\Testing\RefreshDatabase;
8
+use Illuminate\Support\Facades\Event;
9
+use Illuminate\Support\Facades\URL;
10
+use Laravel\Fortify\Features;
11
+use Tests\TestCase;
12
+
13
+class EmailVerificationTest extends TestCase
14
+{
15
+    use RefreshDatabase;
16
+
17
+    public function test_email_verification_screen_can_be_rendered(): void
18
+    {
19
+        if (! Features::enabled(Features::emailVerification())) {
20
+            $this->markTestSkipped('Email verification not enabled.');
21
+
22
+            return;
23
+        }
24
+
25
+        $user = User::factory()->withPersonalCompany()->unverified()->create();
26
+
27
+        $response = $this->actingAs($user)->get('/email/verify');
28
+
29
+        $response->assertStatus(200);
30
+    }
31
+
32
+    public function test_email_can_be_verified(): void
33
+    {
34
+        if (! Features::enabled(Features::emailVerification())) {
35
+            $this->markTestSkipped('Email verification not enabled.');
36
+
37
+            return;
38
+        }
39
+
40
+        Event::fake();
41
+
42
+        $user = User::factory()->unverified()->create();
43
+
44
+        $verificationUrl = URL::temporarySignedRoute(
45
+            'verification.verify',
46
+            now()->addMinutes(60),
47
+            ['id' => $user->id, 'hash' => sha1($user->email)]
48
+        );
49
+
50
+        $response = $this->actingAs($user)->get($verificationUrl);
51
+
52
+        Event::assertDispatched(Verified::class);
53
+
54
+        $this->assertTrue($user->fresh()->hasVerifiedEmail());
55
+        $response->assertRedirect(config('filament.path').'?verified=1');
56
+    }
57
+
58
+    public function test_email_can_not_verified_with_invalid_hash(): void
59
+    {
60
+        if (! Features::enabled(Features::emailVerification())) {
61
+            $this->markTestSkipped('Email verification not enabled.');
62
+
63
+            return;
64
+        }
65
+
66
+        $user = User::factory()->unverified()->create();
67
+
68
+        $verificationUrl = URL::temporarySignedRoute(
69
+            'verification.verify',
70
+            now()->addMinutes(60),
71
+            ['id' => $user->id, 'hash' => sha1('wrong-email')]
72
+        );
73
+
74
+        $this->actingAs($user)->get($verificationUrl);
75
+
76
+        $this->assertFalse($user->fresh()->hasVerifiedEmail());
77
+    }
78
+}

+ 67
- 0
tests/Feature/InviteCompanyEmployeeTest.php 查看文件

@@ -0,0 +1,67 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Illuminate\Support\Facades\Mail;
8
+use Livewire\Livewire;
9
+use Tests\TestCase;
10
+use Wallo\FilamentCompanies\Features;
11
+use Wallo\FilamentCompanies\Http\Livewire\CompanyEmployeeManager;
12
+use Wallo\FilamentCompanies\Mail\CompanyInvitation;
13
+
14
+class InviteCompanyEmployeeTest extends TestCase
15
+{
16
+    use RefreshDatabase;
17
+
18
+    public function test_company_employees_can_be_invited_to_company(): void
19
+    {
20
+        if (! Features::sendsCompanyInvitations()) {
21
+            $this->markTestSkipped('Company invitations not enabled.');
22
+
23
+            return;
24
+        }
25
+
26
+        Mail::fake();
27
+
28
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
29
+
30
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
31
+                        ->set('addCompanyEmployeeForm', [
32
+                            'email' => 'test@example.com',
33
+                            'role' => 'admin',
34
+                        ])->call('addCompanyEmployee');
35
+
36
+        Mail::assertSent(CompanyInvitation::class);
37
+
38
+        $this->assertCount(1, $user->currentCompany->fresh()->companyInvitations);
39
+    }
40
+
41
+    public function test_company_employee_invitations_can_be_cancelled(): void
42
+    {
43
+        if (! Features::sendsCompanyInvitations()) {
44
+            $this->markTestSkipped('Company invitations not enabled.');
45
+
46
+            return;
47
+        }
48
+
49
+        Mail::fake();
50
+
51
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
52
+
53
+        // Add the company employee...
54
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
55
+                        ->set('addCompanyEmployeeForm', [
56
+                            'email' => 'test@example.com',
57
+                            'role' => 'admin',
58
+                        ])->call('addCompanyEmployee');
59
+
60
+        $invitationId = $user->currentCompany->fresh()->companyInvitations->first()->id;
61
+
62
+        // Cancel the company invitation...
63
+        $component->call('cancelCompanyInvitation', $invitationId);
64
+
65
+        $this->assertCount(0, $user->currentCompany->fresh()->companyInvitations);
66
+    }
67
+}

+ 41
- 0
tests/Feature/LeaveCompanyTest.php 查看文件

@@ -0,0 +1,41 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\CompanyEmployeeManager;
10
+
11
+class LeaveCompanyTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_users_can_leave_companies(): void
16
+    {
17
+        $user = User::factory()->withPersonalCompany()->create();
18
+
19
+        $user->currentCompany->users()->attach(
20
+            $otherUser = User::factory()->create(), ['role' => 'admin']
21
+        );
22
+
23
+        $this->actingAs($otherUser);
24
+
25
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
26
+                        ->call('leaveCompany');
27
+
28
+        $this->assertCount(0, $user->currentCompany->fresh()->users);
29
+    }
30
+
31
+    public function test_company_owners_cant_leave_their_own_company(): void
32
+    {
33
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
34
+
35
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
36
+                        ->call('leaveCompany')
37
+                        ->assertHasErrors(['company']);
38
+
39
+        $this->assertNotNull($user->currentCompany->fresh());
40
+    }
41
+}

+ 44
- 0
tests/Feature/PasswordConfirmationTest.php 查看文件

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Tests\TestCase;
8
+
9
+class PasswordConfirmationTest extends TestCase
10
+{
11
+    use RefreshDatabase;
12
+
13
+    public function test_confirm_password_screen_can_be_rendered(): void
14
+    {
15
+        $user = User::factory()->withPersonalCompany()->create();
16
+
17
+        $response = $this->actingAs($user)->get('/user/confirm-password');
18
+
19
+        $response->assertStatus(200);
20
+    }
21
+
22
+    public function test_password_can_be_confirmed(): void
23
+    {
24
+        $user = User::factory()->create();
25
+
26
+        $response = $this->actingAs($user)->post('/user/confirm-password', [
27
+            'password' => 'password',
28
+        ]);
29
+
30
+        $response->assertRedirect();
31
+        $response->assertSessionHasNoErrors();
32
+    }
33
+
34
+    public function test_password_is_not_confirmed_with_invalid_password(): void
35
+    {
36
+        $user = User::factory()->create();
37
+
38
+        $response = $this->actingAs($user)->post('/user/confirm-password', [
39
+            'password' => 'wrong-password',
40
+        ]);
41
+
42
+        $response->assertSessionHasErrors();
43
+    }
44
+}

+ 102
- 0
tests/Feature/PasswordResetTest.php 查看文件

@@ -0,0 +1,102 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Auth\Notifications\ResetPassword;
7
+use Illuminate\Foundation\Testing\RefreshDatabase;
8
+use Illuminate\Support\Facades\Notification;
9
+use Laravel\Fortify\Features;
10
+use Tests\TestCase;
11
+
12
+class PasswordResetTest extends TestCase
13
+{
14
+    use RefreshDatabase;
15
+
16
+    public function test_reset_password_link_screen_can_be_rendered(): void
17
+    {
18
+        if (! Features::enabled(Features::resetPasswords())) {
19
+            $this->markTestSkipped('Password updates are not enabled.');
20
+
21
+            return;
22
+        }
23
+
24
+        $response = $this->get('/forgot-password');
25
+
26
+        $response->assertStatus(200);
27
+    }
28
+
29
+    public function test_reset_password_link_can_be_requested(): void
30
+    {
31
+        if (! Features::enabled(Features::resetPasswords())) {
32
+            $this->markTestSkipped('Password updates are not enabled.');
33
+
34
+            return;
35
+        }
36
+
37
+        Notification::fake();
38
+
39
+        $user = User::factory()->create();
40
+
41
+        $response = $this->post('/forgot-password', [
42
+            'email' => $user->email,
43
+        ]);
44
+
45
+        Notification::assertSentTo($user, ResetPassword::class);
46
+    }
47
+
48
+    public function test_reset_password_screen_can_be_rendered(): void
49
+    {
50
+        if (! Features::enabled(Features::resetPasswords())) {
51
+            $this->markTestSkipped('Password updates are not enabled.');
52
+
53
+            return;
54
+        }
55
+
56
+        Notification::fake();
57
+
58
+        $user = User::factory()->create();
59
+
60
+        $response = $this->post('/forgot-password', [
61
+            'email' => $user->email,
62
+        ]);
63
+
64
+        Notification::assertSentTo($user, ResetPassword::class, function (object $notification) {
65
+            $response = $this->get('/reset-password/'.$notification->token);
66
+
67
+            $response->assertStatus(200);
68
+
69
+            return true;
70
+        });
71
+    }
72
+
73
+    public function test_password_can_be_reset_with_valid_token(): void
74
+    {
75
+        if (! Features::enabled(Features::resetPasswords())) {
76
+            $this->markTestSkipped('Password updates are not enabled.');
77
+
78
+            return;
79
+        }
80
+
81
+        Notification::fake();
82
+
83
+        $user = User::factory()->create();
84
+
85
+        $response = $this->post('/forgot-password', [
86
+            'email' => $user->email,
87
+        ]);
88
+
89
+        Notification::assertSentTo($user, ResetPassword::class, function (object $notification) use ($user) {
90
+            $response = $this->post('/reset-password', [
91
+                'token' => $notification->token,
92
+                'email' => $user->email,
93
+                'password' => 'password',
94
+                'password_confirmation' => 'password',
95
+            ]);
96
+
97
+            $response->assertSessionHasNoErrors();
98
+
99
+            return true;
100
+        });
101
+    }
102
+}

+ 36
- 0
tests/Feature/ProfileInformationTest.php 查看文件

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\UpdateProfileInformationForm;
10
+
11
+class ProfileInformationTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_current_profile_information_is_available(): void
16
+    {
17
+        $this->actingAs($user = User::factory()->create());
18
+
19
+        $component = Livewire::test(UpdateProfileInformationForm::class);
20
+
21
+        $this->assertEquals($user->name, $component->state['name']);
22
+        $this->assertEquals($user->email, $component->state['email']);
23
+    }
24
+
25
+    public function test_profile_information_can_be_updated(): void
26
+    {
27
+        $this->actingAs($user = User::factory()->create());
28
+
29
+        Livewire::test(UpdateProfileInformationForm::class)
30
+                ->set('state', ['name' => 'Test Name', 'email' => 'test@example.com'])
31
+                ->call('updateProfileInformation');
32
+
33
+        $this->assertEquals('Test Name', $user->fresh()->name);
34
+        $this->assertEquals('test@example.com', $user->fresh()->email);
35
+    }
36
+}

+ 59
- 0
tests/Feature/RegistrationTest.php 查看文件

@@ -0,0 +1,59 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use Illuminate\Foundation\Testing\RefreshDatabase;
6
+use Laravel\Fortify\Features;
7
+use Tests\TestCase;
8
+use Wallo\FilamentCompanies\FilamentCompanies;
9
+
10
+class RegistrationTest extends TestCase
11
+{
12
+    use RefreshDatabase;
13
+
14
+    public function test_registration_screen_can_be_rendered(): void
15
+    {
16
+        if (! Features::enabled(Features::registration())) {
17
+            $this->markTestSkipped('Registration support is not enabled.');
18
+
19
+            return;
20
+        }
21
+
22
+        $response = $this->get('/register');
23
+
24
+        $response->assertStatus(200);
25
+    }
26
+
27
+    public function test_registration_screen_cannot_be_rendered_if_support_is_disabled(): void
28
+    {
29
+        if (Features::enabled(Features::registration())) {
30
+            $this->markTestSkipped('Registration support is enabled.');
31
+
32
+            return;
33
+        }
34
+
35
+        $response = $this->get('/register');
36
+
37
+        $response->assertStatus(404);
38
+    }
39
+
40
+    public function test_new_users_can_register(): void
41
+    {
42
+        if (! Features::enabled(Features::registration())) {
43
+            $this->markTestSkipped('Registration support is not enabled.');
44
+
45
+            return;
46
+        }
47
+
48
+        $response = $this->post('/register', [
49
+            'name' => 'Test User',
50
+            'email' => 'test@example.com',
51
+            'password' => 'password',
52
+            'password_confirmation' => 'password',
53
+            'terms' => FilamentCompanies::hasTermsAndPrivacyPolicyFeature(),
54
+        ]);
55
+
56
+        $this->assertAuthenticated();
57
+        $response->assertRedirect(config('filament.path'));
58
+    }
59
+}

+ 45
- 0
tests/Feature/RemoveCompanyEmployeeTest.php 查看文件

@@ -0,0 +1,45 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\CompanyEmployeeManager;
10
+
11
+class RemoveCompanyEmployeeTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_company_employees_can_be_removed_from_companies(): void
16
+    {
17
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
18
+
19
+        $user->currentCompany->users()->attach(
20
+            $otherUser = User::factory()->create(), ['role' => 'admin']
21
+        );
22
+
23
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
24
+                        ->set('companyEmployeeIdBeingRemoved', $otherUser->id)
25
+                        ->call('removeCompanyEmployee');
26
+
27
+        $this->assertCount(0, $user->currentCompany->fresh()->users);
28
+    }
29
+
30
+    public function test_only_company_owner_can_remove_company_employees(): void
31
+    {
32
+        $user = User::factory()->withPersonalCompany()->create();
33
+
34
+        $user->currentCompany->users()->attach(
35
+            $otherUser = User::factory()->create(), ['role' => 'admin']
36
+        );
37
+
38
+        $this->actingAs($otherUser);
39
+
40
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
41
+                        ->set('companyEmployeeIdBeingRemoved', $user->id)
42
+                        ->call('removeCompanyEmployee')
43
+                        ->assertStatus(403);
44
+    }
45
+}

+ 82
- 0
tests/Feature/TwoFactorAuthenticationSettingsTest.php 查看文件

@@ -0,0 +1,82 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Laravel\Fortify\Features;
8
+use Livewire\Livewire;
9
+use Tests\TestCase;
10
+use Wallo\FilamentCompanies\Http\Livewire\TwoFactorAuthenticationForm;
11
+
12
+class TwoFactorAuthenticationSettingsTest extends TestCase
13
+{
14
+    use RefreshDatabase;
15
+
16
+    public function test_two_factor_authentication_can_be_enabled(): void
17
+    {
18
+        if (! Features::canManageTwoFactorAuthentication()) {
19
+            $this->markTestSkipped('Two factor authentication is not enabled.');
20
+
21
+            return;
22
+        }
23
+
24
+        $this->actingAs($user = User::factory()->create());
25
+
26
+        $this->withSession(['auth.password_confirmed_at' => time()]);
27
+
28
+        Livewire::test(TwoFactorAuthenticationForm::class)
29
+                ->call('enableTwoFactorAuthentication');
30
+
31
+        $user = $user->fresh();
32
+
33
+        $this->assertNotNull($user->two_factor_secret);
34
+        $this->assertCount(8, $user->recoveryCodes());
35
+    }
36
+
37
+    public function test_recovery_codes_can_be_regenerated(): void
38
+    {
39
+        if (! Features::canManageTwoFactorAuthentication()) {
40
+            $this->markTestSkipped('Two factor authentication is not enabled.');
41
+
42
+            return;
43
+        }
44
+
45
+        $this->actingAs($user = User::factory()->create());
46
+
47
+        $this->withSession(['auth.password_confirmed_at' => time()]);
48
+
49
+        $component = Livewire::test(TwoFactorAuthenticationForm::class)
50
+                ->call('enableTwoFactorAuthentication')
51
+                ->call('regenerateRecoveryCodes');
52
+
53
+        $user = $user->fresh();
54
+
55
+        $component->call('regenerateRecoveryCodes');
56
+
57
+        $this->assertCount(8, $user->recoveryCodes());
58
+        $this->assertCount(8, array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes()));
59
+    }
60
+
61
+    public function test_two_factor_authentication_can_be_disabled(): void
62
+    {
63
+        if (! Features::canManageTwoFactorAuthentication()) {
64
+            $this->markTestSkipped('Two factor authentication is not enabled.');
65
+
66
+            return;
67
+        }
68
+
69
+        $this->actingAs($user = User::factory()->create());
70
+
71
+        $this->withSession(['auth.password_confirmed_at' => time()]);
72
+
73
+        $component = Livewire::test(TwoFactorAuthenticationForm::class)
74
+                ->call('enableTwoFactorAuthentication');
75
+
76
+        $this->assertNotNull($user->fresh()->two_factor_secret);
77
+
78
+        $component->call('disableTwoFactorAuthentication');
79
+
80
+        $this->assertNull($user->fresh()->two_factor_secret);
81
+    }
82
+}

+ 53
- 0
tests/Feature/UpdateCompanyEmployeeRoleTest.php 查看文件

@@ -0,0 +1,53 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\CompanyEmployeeManager;
10
+
11
+class UpdateCompanyEmployeeRoleTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_company_employee_roles_can_be_updated(): void
16
+    {
17
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
18
+
19
+        $user->currentCompany->users()->attach(
20
+            $otherUser = User::factory()->create(), ['role' => 'admin']
21
+        );
22
+
23
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
24
+                        ->set('managingRoleFor', $otherUser)
25
+                        ->set('currentRole', 'editor')
26
+                        ->call('updateRole');
27
+
28
+        $this->assertTrue($otherUser->fresh()->hasCompanyRole(
29
+            $user->currentCompany->fresh(), 'editor'
30
+        ));
31
+    }
32
+
33
+    public function test_only_company_owner_can_update_company_employee_roles(): void
34
+    {
35
+        $user = User::factory()->withPersonalCompany()->create();
36
+
37
+        $user->currentCompany->users()->attach(
38
+            $otherUser = User::factory()->create(), ['role' => 'admin']
39
+        );
40
+
41
+        $this->actingAs($otherUser);
42
+
43
+        $component = Livewire::test(CompanyEmployeeManager::class, ['company' => $user->currentCompany])
44
+                        ->set('managingRoleFor', $otherUser)
45
+                        ->set('currentRole', 'editor')
46
+                        ->call('updateRole')
47
+                        ->assertStatus(403);
48
+
49
+        $this->assertTrue($otherUser->fresh()->hasCompanyRole(
50
+            $user->currentCompany->fresh(), 'admin'
51
+        ));
52
+    }
53
+}

+ 26
- 0
tests/Feature/UpdateCompanyNameTest.php 查看文件

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Livewire\Livewire;
8
+use Tests\TestCase;
9
+use Wallo\FilamentCompanies\Http\Livewire\UpdateCompanyNameForm;
10
+
11
+class UpdateCompanyNameTest extends TestCase
12
+{
13
+    use RefreshDatabase;
14
+
15
+    public function test_company_names_can_be_updated(): void
16
+    {
17
+        $this->actingAs($user = User::factory()->withPersonalCompany()->create());
18
+
19
+        Livewire::test(UpdateCompanyNameForm::class, ['company' => $user->currentCompany])
20
+                    ->set(['state' => ['name' => 'Test Company']])
21
+                    ->call('updateCompanyName');
22
+
23
+        $this->assertCount(1, $user->fresh()->ownedCompanies);
24
+        $this->assertEquals('Test Company', $user->currentCompany->fresh()->name);
25
+    }
26
+}

+ 62
- 0
tests/Feature/UpdatePasswordTest.php 查看文件

@@ -0,0 +1,62 @@
1
+<?php
2
+
3
+namespace Tests\Feature;
4
+
5
+use App\Models\User;
6
+use Illuminate\Foundation\Testing\RefreshDatabase;
7
+use Illuminate\Support\Facades\Hash;
8
+use Livewire\Livewire;
9
+use Tests\TestCase;
10
+use Wallo\FilamentCompanies\Http\Livewire\UpdatePasswordForm;
11
+
12
+class UpdatePasswordTest extends TestCase
13
+{
14
+    use RefreshDatabase;
15
+
16
+    public function test_password_can_be_updated(): void
17
+    {
18
+        $this->actingAs($user = User::factory()->create());
19
+
20
+        Livewire::test(UpdatePasswordForm::class)
21
+                ->set('state', [
22
+                    'current_password' => 'password',
23
+                    'password' => 'new-password',
24
+                    'password_confirmation' => 'new-password',
25
+                ])
26
+                ->call('updatePassword');
27
+
28
+        $this->assertTrue(Hash::check('new-password', $user->fresh()->password));
29
+    }
30
+
31
+    public function test_current_password_must_be_correct(): void
32
+    {
33
+        $this->actingAs($user = User::factory()->create());
34
+
35
+        Livewire::test(UpdatePasswordForm::class)
36
+                ->set('state', [
37
+                    'current_password' => 'wrong-password',
38
+                    'password' => 'new-password',
39
+                    'password_confirmation' => 'new-password',
40
+                ])
41
+                ->call('updatePassword')
42
+                ->assertHasErrors(['current_password']);
43
+
44
+        $this->assertTrue(Hash::check('password', $user->fresh()->password));
45
+    }
46
+
47
+    public function test_new_passwords_must_match(): void
48
+    {
49
+        $this->actingAs($user = User::factory()->create());
50
+
51
+        Livewire::test(UpdatePasswordForm::class)
52
+                ->set('state', [
53
+                    'current_password' => 'password',
54
+                    'password' => 'new-password',
55
+                    'password_confirmation' => 'wrong-password',
56
+                ])
57
+                ->call('updatePassword')
58
+                ->assertHasErrors(['password']);
59
+
60
+        $this->assertTrue(Hash::check('password', $user->fresh()->password));
61
+    }
62
+}

+ 14
- 5
vite.config.js 查看文件

@@ -1,11 +1,20 @@
1
-import { defineConfig } from 'vite';
2
-import laravel from 'laravel-vite-plugin';
1
+import { defineConfig } from 'vite'
2
+import laravel, { refreshPaths } from 'laravel-vite-plugin'
3 3
 
4 4
 export default defineConfig({
5 5
     plugins: [
6 6
         laravel({
7
-            input: ['resources/css/app.css', 'resources/js/app.js'],
8
-            refresh: true,
7
+            input: [
8
+                'resources/css/app.css',
9
+                'resources/js/app.js',
10
+                'resources/css/filament.css',
11
+            ],
12
+            refresh: [
13
+                ...refreshPaths,
14
+                'app/Http/Livewire/**',
15
+                'app/Forms/Components/**',
16
+                'app/Tables/Columns/**',
17
+            ],
9 18
         }),
10 19
     ],
11
-});
20
+})

正在加载...
取消
保存