├── public
├── favicon.ico
├── robots.txt
├── images
│ └── default-user.png
├── mix-manifest.json
├── build
│ └── manifest.json
├── .htaccess
├── web.config
└── index.php
├── routes
├── api.php
├── console.php
├── channels.php
├── admin.php
└── web.php
├── bootstrap
├── cache
│ └── .gitignore
└── app.php
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
├── debugbar
│ └── .gitignore
├── framework
│ ├── testing
│ │ └── .gitignore
│ ├── views
│ │ └── .gitignore
│ ├── cache
│ │ ├── data
│ │ │ └── .gitignore
│ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ └── .gitignore
└── clockwork
│ └── .gitignore
├── database
├── .gitignore
├── seeders
│ ├── TeamSeeder.php
│ ├── DatabaseSeeder.php
│ └── UserSeeder.php
├── factories
│ ├── TeamFactory.php
│ ├── ActivityFactory.php
│ └── UserFactory.php
└── migrations
│ ├── 2014_10_11_000000_create_teams_table.php
│ ├── 2014_10_12_100000_create_password_resets_table.php
│ ├── 2023_02_15_121808_create_team_user_table.php
│ ├── 2023_02_15_122036_create_team_invitations_table.php
│ ├── 2019_08_19_000000_create_failed_jobs_table.php
│ ├── 2021_12_22_121828_add_event_column_to_activity_log_table.php
│ ├── 2021_12_22_121829_add_batch_uuid_column_to_activity_log_table.php
│ ├── 2021_11_09_084508_create_jobs_table.php
│ ├── 2014_10_12_000000_create_users_table.php
│ └── 2021_12_22_121827_create_activity_log_table.php
├── tests
├── Feature
│ └── Http
│ │ └── Controllers
│ │ ├── Admin
│ │ ├── UserController
│ │ │ └── EditTest.php
│ │ └── DashboardControllerTest.php
│ │ ├── DashboardControllerTest.php
│ │ ├── ActivityControllerTest.php
│ │ ├── Auth
│ │ └── EmailVerificationPromptControllerTest.php
│ │ └── Account
│ │ └── ProfileControllerTest.php
├── CreatesApplication.php
├── Unit
│ ├── Services
│ │ ├── MemberNavTest.php
│ │ └── AdminNavTest.php
│ ├── Providers
│ │ ├── EventServiceProviderTest.php
│ │ └── AppServiceProviderTest.php
│ ├── Rules
│ │ └── PasswordCheckRuleTest.php
│ ├── Notifications
│ │ ├── PasswordUpdatedNotificationTest.php
│ │ ├── EmailUpdateWarningNotificationTest.php
│ │ └── EmailUpdateRequestNotificationTest.php
│ ├── Http
│ │ ├── Middleware
│ │ │ └── AdminTest.php
│ │ └── Requests
│ │ │ └── Auth
│ │ │ ├── PasswordResetLinkRequestTest.php
│ │ │ ├── LoginRequestTest.php
│ │ │ └── NewPasswordRequestTest.php
│ ├── QueryBuilders
│ │ ├── UserQueryBuilderTest.php
│ │ └── FilterableTest.php
│ ├── Models
│ │ └── Concerns
│ │ │ └── LogsDeleteActivityTest.php
│ ├── Listeners
│ │ └── ResizeImageListenerTest.php
│ └── Components
│ │ └── SidebarTest.php
├── TestCase.php
└── helpers.php
├── config
├── castra.php
├── cors.php
├── services.php
├── view.php
├── activitylog.php
├── hashing.php
└── broadcasting.php
├── resources
├── views
│ ├── dashboards
│ │ └── index.blade.php
│ ├── admin
│ │ └── dashboards
│ │ │ └── index.blade.php
│ ├── components
│ │ ├── svg
│ │ │ ├── chevron-up.blade.php
│ │ │ ├── path.blade.php
│ │ │ ├── chevron-down.blade.php
│ │ │ ├── back.blade.php
│ │ │ ├── hamburger.blade.php
│ │ │ ├── svg.blade.php
│ │ │ ├── file-upload.blade.php
│ │ │ ├── edit.blade.php
│ │ │ ├── dashboard.blade.php
│ │ │ ├── users.blade.php
│ │ │ ├── trash.blade.php
│ │ │ ├── undo.blade.php
│ │ │ ├── search.blade.php
│ │ │ ├── error.blade.php
│ │ │ ├── info.blade.php
│ │ │ ├── warning.blade.php
│ │ │ ├── success.blade.php
│ │ │ ├── log.blade.php
│ │ │ └── logo.blade.php
│ │ ├── table
│ │ │ ├── td.blade.php
│ │ │ ├── th.blade.php
│ │ │ ├── delete.blade.php
│ │ │ ├── order-by.blade.php
│ │ │ ├── search-button.blade.php
│ │ │ ├── search-select.blade.php
│ │ │ ├── search-input.blade.php
│ │ │ ├── header-ordable.blade.php
│ │ │ ├── filters.blade.php
│ │ │ └── table.blade.php
│ │ ├── error.blade.php
│ │ ├── input.blade.php
│ │ ├── button.blade.php
│ │ ├── auth-session-status.blade.php
│ │ ├── label.blade.php
│ │ ├── label-checkbox.blade.php
│ │ ├── dropdown-link.blade.php
│ │ ├── layouts
│ │ │ ├── guest.blade.php
│ │ │ ├── auth.blade.php
│ │ │ ├── sidebar.blade.php
│ │ │ └── header.blade.php
│ │ ├── status
│ │ │ ├── inline.blade.php
│ │ │ ├── body.blade.php
│ │ │ └── layout.blade.php
│ │ └── dropdown.blade.php
│ ├── layouts
│ │ ├── _profile-image.blade.php
│ │ └── app.blade.php
│ ├── errors
│ │ ├── 401.blade.php
│ │ ├── 429.blade.php
│ │ ├── 500.blade.php
│ │ ├── 403.blade.php
│ │ └── 404.blade.php
│ ├── accounts
│ │ ├── _image.blade.php
│ │ └── images
│ │ │ └── _update_stream.blade.php
│ └── auth
│ │ ├── forgot-password.blade.php
│ │ ├── verify-email.blade.php
│ │ ├── reset-password.blade.php
│ │ ├── register.blade.php
│ │ └── login.blade.php
├── css
│ └── app.css
├── js
│ ├── app.js
│ ├── turbo.js
│ ├── bootstrap.js
│ ├── alpine.js
│ └── elements
│ │ └── turbo-echo-stream-tag.js
└── lang
│ └── en
│ ├── pagination.php
│ ├── auth.php
│ └── passwords.php
├── tformat.json
├── stubs
├── observer.plain.stub
├── test.unit.stub
├── model.pivot.stub
├── policy.plain.stub
├── controller.plain.stub
├── model.stub
├── test.stub
├── seeder.stub
├── provider.stub
├── job.stub
├── controller.invokable.stub
├── scope.stub
├── migration.stub
├── resource.stub
├── view-component.stub
├── console.stub
├── resource-collection.stub
├── rule.stub
├── middleware.stub
├── cast.inbound.stub
├── factory.stub
├── request.stub
├── migration.create.stub
├── migration.update.stub
├── job.queued.stub
├── controller.singleton.api.stub
├── cast.stub
├── observer.stub
├── controller.api.stub
├── event.stub
├── controller.model.api.stub
├── controller.singleton.stub
├── controller.nested.singleton.api.stub
├── markdown-notification.stub
├── controller.stub
├── mail.stub
├── markdown-mail.stub
├── policy.stub
├── controller.nested.api.stub
├── controller.model.stub
├── notification.stub
├── controller.nested.singleton.stub
└── controller.nested.stub
├── .prettierrc
├── postcss.config.js
├── .gitattributes
├── app
├── Enums
│ ├── UserRoles.php
│ └── ActivityEvents.php
├── Models
│ ├── Team.php
│ ├── Concerns
│ │ └── LogsDeleteActivity.php
│ ├── Activity.php
│ └── User.php
├── Http
│ ├── Middleware
│ │ ├── EncryptCookies.php
│ │ ├── PreventRequestsDuringMaintenance.php
│ │ ├── TrustHosts.php
│ │ ├── TrimStrings.php
│ │ ├── VerifyCsrfToken.php
│ │ ├── Authenticate.php
│ │ ├── Admin.php
│ │ ├── TrustProxies.php
│ │ └── RedirectIfAuthenticated.php
│ ├── Controllers
│ │ ├── DashboardController.php
│ │ ├── Account
│ │ │ ├── ProfileController.php
│ │ │ ├── PasswordController.php
│ │ │ ├── EmailController.php
│ │ │ ├── VerifyNewEmailController.php
│ │ │ └── ImageController.php
│ │ ├── Admin
│ │ │ ├── DashboardController.php
│ │ │ └── UserController.php
│ │ ├── RestoredItemController.php
│ │ ├── Controller.php
│ │ ├── Auth
│ │ │ ├── EmailVerificationPromptController.php
│ │ │ ├── EmailVerificationNotificationController.php
│ │ │ ├── VerifyEmailController.php
│ │ │ ├── PasswordResetLinkController.php
│ │ │ ├── AuthenticatedSessionController.php
│ │ │ ├── RegisteredUserController.php
│ │ │ └── NewPasswordController.php
│ │ ├── TeamController.php
│ │ └── ActivityController.php
│ └── Requests
│ │ ├── StoreTeamRequest.php
│ │ ├── UpdateTeamRequest.php
│ │ ├── Auth
│ │ ├── PasswordResetLinkRequest.php
│ │ ├── RegisterUserRequest.php
│ │ └── NewPasswordRequest.php
│ │ ├── Account
│ │ ├── UpdateImageRequest.php
│ │ ├── UpdatePasswordRequest.php
│ │ └── UpdateEmailRequest.php
│ │ └── Admin
│ │ └── StoreDeletedUserRequest.php
├── View
│ └── Components
│ │ ├── AppLayout.php
│ │ └── Sidebar.php
├── Services
│ ├── MemberNav.php
│ ├── AdminNav.php
│ └── SignedUrlGenerator.php
├── Providers
│ ├── BroadcastServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── AppServiceProvider.php
│ ├── EventServiceProvider.php
│ ├── BladeServiceProvider.php
│ └── RouteServiceProvider.php
├── Events
│ └── ProfileImageUploaded.php
├── Console
│ └── Kernel.php
├── Rules
│ ├── PasswordCheckRule.php
│ └── PasswordRule.php
├── Scopes
│ └── VisibleToScope.php
├── Exceptions
│ └── Handler.php
├── QueryBuilders
│ ├── Filterable.php
│ ├── UserQueryBuilder.php
│ └── ActivityQueryBuilder.php
├── Listeners
│ └── ResizeImageListener.php
├── Notifications
│ ├── PasswordUpdatedNotification.php
│ ├── EmailUpdateWarningNotification.php
│ └── EmailUpdateRequestNotification.php
└── helpers.php
├── .gitignore
├── vite.config.js
├── pint.json
├── .styleci.yml
├── .editorconfig
├── server.php
├── phpstan.neon
├── tailwind.config.js
├── .php-cs-fixer.dist.php
├── package.json
├── licence.md
├── .env.example
├── phpunit.xml
├── phpunit.xml.bak
└── artisan
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | 10,
5 | ];
6 |
--------------------------------------------------------------------------------
/resources/views/dashboards/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/resources/views/admin/dashboards/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tformat.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel",
3 | "disabled": ["SpaceAfterBladeDirectives"]
4 | }
5 |
--------------------------------------------------------------------------------
/public/images/default-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jcergolj/castra/HEAD/public/images/default-user.png
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/app.js": "/js/app.js",
3 | "/css/app.css": "/css/app.css"
4 | }
5 |
--------------------------------------------------------------------------------
/stubs/observer.plain.stub:
--------------------------------------------------------------------------------
1 | '« Previous',
5 | 'next' => 'Next »',
6 | ];
7 |
--------------------------------------------------------------------------------
/resources/views/components/svg/chevron-up.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/components/svg/path.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.css linguist-vendored
3 | *.scss linguist-vendored
4 | *.js linguist-vendored
5 | CHANGELOG.md export-ignore
6 |
--------------------------------------------------------------------------------
/resources/views/components/svg/chevron-down.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/js/turbo.js:
--------------------------------------------------------------------------------
1 | import * as Turbo from '@hotwired/turbo';
2 |
3 | window.Turbo = Turbo;
4 |
5 | Turbo.start();
6 |
7 | export default Turbo;
8 |
--------------------------------------------------------------------------------
/resources/views/components/svg/back.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/components/svg/hamburger.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/components/table/td.blade.php:
--------------------------------------------------------------------------------
1 |
merge(['class' => 'px-5 py-5 bg-white text-sm']) }}>
2 | {{ $slot }}
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/layouts/_profile-image.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/stubs/test.unit.stub:
--------------------------------------------------------------------------------
1 |
5 | {{ $message }}
6 |
7 | @enderror
8 |
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | compiled.php
2 | config.php
3 | down
4 | events.scanned.php
5 | maintenance.php
6 | routes.php
7 | routes.scanned.php
8 | schedule-*
9 | services.json
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /public/storage
3 | /vendor
4 | .env
5 | .env.backup
6 | .phpunit.result.cache
7 | docker-compose.override.yml
8 | npm-debug.log
9 | .php-cs-fixer.cache
10 |
--------------------------------------------------------------------------------
/stubs/model.pivot.stub:
--------------------------------------------------------------------------------
1 | merge(['viewBox' => '0 0 24 24']) }}>
3 | {{ $slot }}
4 |
5 |
--------------------------------------------------------------------------------
/app/Enums/ActivityEvents.php:
--------------------------------------------------------------------------------
1 | false])
2 |
3 | merge(['class' => 'form-input w-full rounded-md focus:border-blue-600']) !!}>
4 |
--------------------------------------------------------------------------------
/resources/views/components/svg/file-upload.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import laravel from 'laravel-vite-plugin';
3 |
4 | export default defineConfig({
5 | plugins: [laravel(['resources/css/app.css', 'resources/js/app.js'])],
6 | });
7 |
--------------------------------------------------------------------------------
/resources/views/components/button.blade.php:
--------------------------------------------------------------------------------
1 | merge(['type' => 'submit', 'class' => 'py-2 px-4 text-center rounded-md bg-blue-600 hover:bg-blue-500 text-white']) }}>
3 | {{ $slot }}
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/components/svg/edit.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/stubs/policy.plain.stub:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/components/svg/users.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/stubs/controller.plain.stub:
--------------------------------------------------------------------------------
1 | merge(['class' => 'text-green-500 font-semibold']) }}>
5 | {{ $status['message'] }}
6 |
7 | @endif
8 |
--------------------------------------------------------------------------------
/resources/views/components/label.blade.php:
--------------------------------------------------------------------------------
1 | @props(['value' => null])
2 |
3 |
4 | @if ($value !== null)
5 | {{ $value }}
6 | @endif
7 |
8 | {{ $slot }}
9 |
10 |
--------------------------------------------------------------------------------
/resources/views/components/table/th.blade.php:
--------------------------------------------------------------------------------
1 | merge(['class' => 'px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider']) }}>
3 | {{ $slot }}
4 |
5 |
--------------------------------------------------------------------------------
/database/seeders/TeamSeeder.php:
--------------------------------------------------------------------------------
1 | merge(['class' => 'inline-flex items-center']) }}>
4 | {{ $slot }}
5 | {{ $value }}
6 |
7 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | php:
2 | preset: laravel
3 | disabled:
4 | - no_unused_imports
5 | finder:
6 | not-name:
7 | - index.php
8 | - server.php
9 | js:
10 | finder:
11 | not-name:
12 | - webpack.mix.js
13 | css: true
14 |
--------------------------------------------------------------------------------
/resources/views/components/svg/trash.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/stubs/model.stub:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
5 | 'password' => 'The provided password is incorrect.',
6 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
7 | ];
8 |
--------------------------------------------------------------------------------
/resources/views/components/dropdown-link.blade.php:
--------------------------------------------------------------------------------
1 | merge(['class' => 'block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>
3 | {{ $slot }}
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/components/table/delete.blade.php:
--------------------------------------------------------------------------------
1 | @props(['route' => null])
2 |
3 |
9 |
--------------------------------------------------------------------------------
/resources/views/components/svg/undo.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/components/svg/search.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 | */
10 | protected $except = [];
11 | }
12 |
--------------------------------------------------------------------------------
/resources/views/components/table/order-by.blade.php:
--------------------------------------------------------------------------------
1 | @props(['orderBy', 'field', 'orderByDirection'])
2 |
3 | @if ($orderBy === $field && $orderByDirection === 'asc')
4 |
5 | @elseif ($orderBy === $field && $orderByDirection === 'desc')
6 |
7 | @endif
8 |
--------------------------------------------------------------------------------
/stubs/seeder.stub:
--------------------------------------------------------------------------------
1 | call([
12 | UserSeeder::class,
13 | ]);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/resources/views/components/table/search-button.blade.php:
--------------------------------------------------------------------------------
1 |
3 | Search
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/components/table/search-select.blade.php:
--------------------------------------------------------------------------------
1 |
3 | {{ $slot }}
4 |
5 |
--------------------------------------------------------------------------------
/public/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources/css/app.css": {
3 | "file": "assets/app-ebe451a1.css",
4 | "src": "resources/css/app.css",
5 | "isEntry": true
6 | },
7 | "resources/js/app.js": {
8 | "file": "assets/app-532e15ff.js",
9 | "src": "resources/js/app.js",
10 | "isEntry": true
11 | }
12 | }
--------------------------------------------------------------------------------
/stubs/provider.stub:
--------------------------------------------------------------------------------
1 | */
10 | protected $except = [];
11 | }
12 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Account/ProfileController.php:
--------------------------------------------------------------------------------
1 | 'Your password has been reset!',
5 | 'sent' => 'We have emailed your password reset link!',
6 | 'throttled' => 'Please wait before retrying.',
7 | 'token' => 'This password reset token is invalid.',
8 | 'user' => "We can't find a user with that email address.",
9 | ];
10 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustHosts.php:
--------------------------------------------------------------------------------
1 | allSubdomainsOfApplicationUrl(),
13 | ];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/controller.invokable.stub:
--------------------------------------------------------------------------------
1 | User::factory(),
14 | ];
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/stubs/migration.stub:
--------------------------------------------------------------------------------
1 | 'svg.dashboard', 'route' => 'dashboards.index', 'title' => 'Dashboard'],
11 | ['svg' => 'svg.log', 'route' => 'activities.index', 'title' => 'Log'],
12 | ];
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrimStrings.php:
--------------------------------------------------------------------------------
1 | */
10 | protected $except = [
11 | 'current_password',
12 | 'password',
13 | 'password_confirmation',
14 | ];
15 | }
16 |
--------------------------------------------------------------------------------
/resources/views/errors/401.blade.php:
--------------------------------------------------------------------------------
1 | @php
2 | $title = '401 - Unauthorized';
3 | $message = $exception->getMessage() ?: 'You are not authorized.';
4 | @endphp
5 |
6 |
7 |
8 |
{{ $title }}
9 | {{ $message }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/views/errors/429.blade.php:
--------------------------------------------------------------------------------
1 | @php
2 | $title = '429 - Too many requests';
3 | $message = $exception->getMessage() ?: 'Too many requests.';
4 | @endphp
5 |
6 |
7 |
8 |
{{ $title }}
9 | {{ $message }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/Http/Middleware/VerifyCsrfToken.php:
--------------------------------------------------------------------------------
1 |
13 | */
14 | protected $except = [];
15 | }
16 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | getMessage() ?: 'Something went wrong on our end.';
4 | @endphp
5 |
6 |
7 |
8 |
{{ $title }}
9 | {{ $message }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/stubs/resource.stub:
--------------------------------------------------------------------------------
1 |
12 | */
13 | public function toArray(Request $request): array
14 | {
15 | return parent::toArray($request);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/resources/views/components/svg/error.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/components/svg/info.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/components/svg/warning.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/resources/views/errors/403.blade.php:
--------------------------------------------------------------------------------
1 | @php
2 | $title = '403 - Forbidden';
3 | $message = $exception->getMessage() ?: 'You are not allowed to see this page.';
4 | @endphp
5 |
6 |
7 |
8 |
{{ $title }}
9 | {{ $message }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/resources/views/errors/404.blade.php:
--------------------------------------------------------------------------------
1 | @php
2 | $title = 'Page not Found';
3 | $message = $exception->getMessage() ?: 'Page you are looking for doesn\'t exists.';
4 | @endphp
5 |
6 |
7 |
8 |
{{ $title }}
9 | {{ $message }}
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/Http/Requests/StoreTeamRequest.php:
--------------------------------------------------------------------------------
1 | search) }}"
2 | class="appearance-none rounded-r rounded-l sm:rounded-l-none border border-gray-400 border-b block pl-8 pr-6 py-2 w-full bg-white text-sm placeholder-gray-400 text-gray-700 focus:bg-white focus:placeholder-gray-600 focus:text-gray-700 focus:outline-none"
3 | placeholder="Search" />
4 |
--------------------------------------------------------------------------------
/app/Http/Controllers/RestoredItemController.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/stubs/console.stub:
--------------------------------------------------------------------------------
1 |
12 | */
13 | public function toArray(Request $request): array
14 | {
15 | return parent::toArray($request);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/Providers/AuthServiceProvider.php:
--------------------------------------------------------------------------------
1 | */
10 | protected $policies = [];
11 |
12 | public function boot(): void
13 | {
14 | $this->registerPolicies();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Authenticate.php:
--------------------------------------------------------------------------------
1 | expectsJson()) {
13 | return route('login');
14 | }
15 |
16 | return null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/Admin.php:
--------------------------------------------------------------------------------
1 | user()->isAdmin()) {
14 | return $next($request);
15 | }
16 |
17 | abort(Response::HTTP_FORBIDDEN, 'You are not an admin.');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Http/Requests/Auth/PasswordResetLinkRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'email'],
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | window._ = _;
3 |
4 | /**
5 | * We'll load the axios HTTP library which allows us to easily issue requests
6 | * to our Laravel back-end. This library automatically handles sending the
7 | * CSRF token as a header based on the value of the "XSRF" token cookie.
8 | */
9 |
10 | import axios from 'axios';
11 | window.axios = axios;
12 |
13 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
14 |
--------------------------------------------------------------------------------
/routes/admin.php:
--------------------------------------------------------------------------------
1 | name('admin.')
9 | ->group(function () {
10 | Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboards.index');
11 |
12 | Route::resource('users', UserController::class)->only(['index', 'destroy']);
13 | });
14 |
--------------------------------------------------------------------------------
/stubs/middleware.stub:
--------------------------------------------------------------------------------
1 | 'svg.dashboard', 'route' => 'admin.dashboards.index', 'title' => 'Dashboard'],
11 | ['svg' => 'svg.users', 'route' => 'admin.users.index', 'title' => 'Users'],
12 | ['svg' => 'svg.log', 'route' => 'activities.index', 'title' => 'Log'],
13 | ];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/stubs/cast.inbound.stub:
--------------------------------------------------------------------------------
1 | $attributes
12 | */
13 | public function set(Model $model, string $key, mixed $value, array $attributes): mixed
14 | {
15 | return $value;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/stubs/factory.stub:
--------------------------------------------------------------------------------
1 |
9 | */
10 | class {{ factory }}Factory extends Factory
11 | {
12 | /**
13 | * @return array
14 | */
15 | public function definition(): array
16 | {
17 | return [
18 | //
19 | ];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/stubs/request.stub:
--------------------------------------------------------------------------------
1 |
16 | */
17 | public function rules(): array
18 | {
19 | return [
20 | //
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Events/ProfileImageUploaded.php:
--------------------------------------------------------------------------------
1 | user = $user;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/resources/views/components/layouts/guest.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ __('Admin Dashboard') }}
7 |
8 |
9 | {{ $slot }}
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/resources/views/components/svg/logo.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/app/Services/SignedUrlGenerator.php:
--------------------------------------------------------------------------------
1 | $user->id, 'new_email' => $newEmail]
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/stubs/migration.create.stub:
--------------------------------------------------------------------------------
1 | id();
13 | $table->timestamps();
14 | });
15 | }
16 |
17 | public function down(): void
18 | {
19 | Schema::dropIfExists('{{ table }}');
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/stubs/migration.update.stub:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ $slot }}
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/resources/views/accounts/_image.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/EmailVerificationPromptController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()) {
14 | return redirect()->intended(RouteServiceProvider::HOME);
15 | }
16 |
17 | return view('auth.verify-email');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/Http/Requests/Account/UpdateImageRequest.php:
--------------------------------------------------------------------------------
1 | [
18 | 'required',
19 | 'image',
20 | 'dimensions:min_width=100,min_height=100',
21 | ],
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Http/Requests/Auth/RegisterUserRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'string', 'email', 'max:255', 'unique:users'],
19 | 'password' => [new PasswordRule($this->password_confirmation)],
20 | ];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Http/Requests/Auth/NewPasswordRequest.php:
--------------------------------------------------------------------------------
1 | ['required'],
19 | 'email' => ['required', 'email'],
20 | 'password' => [new PasswordRule($this->password_confirmation)],
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/Unit/Services/MemberNavTest.php:
--------------------------------------------------------------------------------
1 | build();
16 | $expected = [
17 | ['svg' => 'svg.dashboard', 'route' => 'dashboards.index', 'title' => 'Dashboard'],
18 | ];
19 |
20 | $this->assertSame(sort($expected), sort($actual));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | command('inspire')->hourly();
16 | }
17 |
18 | protected function commands(): void
19 | {
20 | $this->load(__DIR__.'/Commands');
21 |
22 | require base_path('routes/console.php');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Rules/PasswordCheckRule.php:
--------------------------------------------------------------------------------
1 | user->password);
18 | }
19 |
20 | public function message(): string
21 | {
22 | return trans('validation.password_check');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Scopes/VisibleToScope.php:
--------------------------------------------------------------------------------
1 | isAdmin()) {
18 | return $builder;
19 | }
20 |
21 | return $builder->where($this->field, user()->id);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server.php:
--------------------------------------------------------------------------------
1 |
7 | */
8 | $uri = urldecode(
9 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
10 | );
11 |
12 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the
13 | // built-in PHP web server. This provides a convenient way to test a Laravel
14 | // application without having installed a "real" web server software here.
15 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
16 | return false;
17 | }
18 |
19 | require_once __DIR__.'/public/index.php';
20 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/nunomaduro/larastan/extension.neon
3 |
4 | parameters:
5 | reportUnmatchedIgnoredErrors: false
6 | paths:
7 | - app
8 |
9 | # The level 8 is the highest level
10 | level: 5
11 |
12 | ignoreErrors:
13 | - '#Unsafe usage of new static#'
14 | - '#Access to an undefined property Spatie\\Activitylog\\Models\\Activity::\$event.#'
15 | - '#Access to an undefined property Spatie\\Activitylog\\Models\\Activity::\$properties.#'
16 |
17 | excludePaths:
18 | - ./*/*/FileToBeExcluded.php
19 |
20 | checkMissingIterableValueType: false
21 |
--------------------------------------------------------------------------------
/stubs/job.queued.stub:
--------------------------------------------------------------------------------
1 | > */
11 | protected $dontReport = [];
12 |
13 | /** @var array */
14 | protected $dontFlash = [
15 | 'current_password',
16 | 'password',
17 | 'password_confirmation',
18 | ];
19 |
20 | public function register(): void
21 | {
22 | $this->reportable(function (Throwable $exception) {
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/resources/views/components/table/header-ordable.blade.php:
--------------------------------------------------------------------------------
1 | @props(['route' => null, 'orderBy' => null, 'orderByDirection' => 'asc', 'field' => null])
2 |
3 |
5 |
6 |
7 | {{ $slot }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/Unit/Providers/EventServiceProviderTest.php:
--------------------------------------------------------------------------------
1 | assertListening(Registered::class, SendEmailVerificationNotification::class);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/Unit/Rules/PasswordCheckRuleTest.php:
--------------------------------------------------------------------------------
1 | bcrypt('password')]));
16 |
17 | $this->assertTrue($rule->passes('password', 'password'));
18 | $this->assertFalse($rule->passes('password', 'invalid-password'));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/resources/views/components/status/inline.blade.php:
--------------------------------------------------------------------------------
1 | @props(['messageBag' => null])
2 |
3 | @if (session('status') && session('status')['message_bag'] === $messageBag)
4 |
9 | {{ session('status')['message'] }}
10 |
11 |
12 | forget('status'); ?>
13 | @endif
14 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_11_000000_create_teams_table.php:
--------------------------------------------------------------------------------
1 | id();
13 | $table->foreignId('user_id')->index();
14 | $table->string('name');
15 | $table->timestamps();
16 | });
17 | }
18 |
19 | public function down(): void
20 | {
21 | Schema::dropIfExists('teams');
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | app->make(Request::class);
18 |
19 | return $request->setUserResolver(function () use ($user) {
20 | return $user;
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/resources/js/alpine.js:
--------------------------------------------------------------------------------
1 | import Alpine from 'alpinejs';
2 |
3 | document.addEventListener('turbo:before-render', () => {
4 | let permanents = document.querySelectorAll('[data-turbo-permanent]');
5 | let undos = Array.from(permanents).map((el) => {
6 | el._x_ignore = true;
7 | return () => {
8 | delete el._x_ignore;
9 | };
10 | });
11 |
12 | document.addEventListener('turbo:render', function handler() {
13 | while (undos.length) undos.shift()();
14 | document.removeEventListener('turbo:render', handler);
15 | });
16 | });
17 |
18 | window.Alpine = Alpine;
19 |
20 | Alpine.start();
21 |
22 | export default Alpine;
23 |
--------------------------------------------------------------------------------
/resources/views/components/status/body.blade.php:
--------------------------------------------------------------------------------
1 | @props(['color' => null])
2 |
3 |
10 |
11 |
12 | {{ $title }}
13 |
14 |
15 | {{ $message }}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/stubs/cast.stub:
--------------------------------------------------------------------------------
1 | $attributes
12 | */
13 | public function get(Model $model, string $key, mixed $value, array $attributes): mixed
14 | {
15 | return $value;
16 | }
17 |
18 | /**
19 | * @param array $attributes
20 | */
21 | public function set(Model $model, string $key, mixed $value, array $attributes): mixed
22 | {
23 | return $value;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/helpers.php:
--------------------------------------------------------------------------------
1 | create($attributes);
11 | }
12 |
13 | /**
14 | * User seeder.
15 | */
16 | function make_user($attributes = []): User
17 | {
18 | return User::factory()->make($attributes);
19 | }
20 |
21 | /**
22 | * Member seeder.
23 | */
24 | function create_member($attributes = []): User
25 | {
26 | return create_user($attributes);
27 | }
28 |
29 | /**
30 | * Admin seeder.
31 | */
32 | function create_admin($attributes = []): User
33 | {
34 | return User::factory()->admin()->create($attributes);
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_100000_create_password_resets_table.php:
--------------------------------------------------------------------------------
1 | string('email')->index();
13 | $table->string('token');
14 | $table->timestamp('created_at')->nullable();
15 | });
16 | }
17 |
18 | public function down(): void
19 | {
20 | Schema::dropIfExists('password_resets');
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/resources/views/accounts/images/_update_stream.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | @include('accounts._image')
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/resources/views/layouts/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ config('app.name', 'Laravel') }}
11 |
12 | @vite(['resources/css/app.css', 'resources/js/app.js'])
13 |
14 | @stack('scripts')
15 |
16 |
17 |
18 |
19 |
20 | {{ $slot }}
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 |
3 | module.exports = {
4 | content: [
5 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
6 | './storage/framework/views/*.php',
7 | './resources/views/**/*.blade.php',
8 | ],
9 |
10 | theme: {
11 | extend: {
12 | fontFamily: {
13 | sans: ['Nunito', ...defaultTheme.fontFamily.sans],
14 | },
15 | },
16 | },
17 |
18 | variants: {
19 | extend: {
20 | opacity: ['disabled'],
21 | },
22 | },
23 |
24 | plugins: [require('@tailwindcss/forms')],
25 | };
26 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Redirect Trailing Slashes If Not A Folder...
13 | RewriteCond %{REQUEST_FILENAME} !-d
14 | RewriteCond %{REQUEST_URI} (.+)/$
15 | RewriteRule ^ %1 [L,R=301]
16 |
17 | # Send Requests To Front Controller...
18 | RewriteCond %{REQUEST_FILENAME} !-d
19 | RewriteCond %{REQUEST_FILENAME} !-f
20 | RewriteRule ^ index.php [L]
21 |
22 |
--------------------------------------------------------------------------------
/stubs/observer.stub:
--------------------------------------------------------------------------------
1 | app->isProduction());
23 |
24 | if (env('PREVENT_STRAY_REQUESTS', false)) {
25 | Http::preventStrayRequests();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/stubs/controller.api.stub:
--------------------------------------------------------------------------------
1 | filter(function ($args, $method) {
12 | return method_exists($this, $method);
13 | })->each(function ($args, $method) {
14 | $this->call($method, $args);
15 | });
16 |
17 | return $this;
18 | }
19 |
20 | protected function call($method, $args): mixed
21 | {
22 | if (is_array($args)) {
23 | return $this->$method($args[0], $args[1]);
24 | }
25 |
26 | return $this->$method($args);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Requests/Admin/StoreDeletedUserRequest.php:
--------------------------------------------------------------------------------
1 | ['required', 'array'],
18 | 'ids.*' => [
19 | 'exists:users,id', function ($attribute, $value, $fail) {
20 | if ($value === user()->id) {
21 | $fail('You cannot delete yourself.');
22 | }
23 | },
24 | ],
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Http/Requests/Account/UpdatePasswordRequest.php:
--------------------------------------------------------------------------------
1 | [
20 | new PasswordRule($this->new_password_confirmation),
21 | ],
22 | 'current_password' => [
23 | 'required',
24 | new PasswordCheckRule($this->user()),
25 | ],
26 | ];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Unit/Services/AdminNavTest.php:
--------------------------------------------------------------------------------
1 | build();
16 | $expected = [
17 | ['svg' => 'svg.dashboard', 'route' => 'admin.dashboards.index', 'title' => 'Dashboard'],
18 | ['svg' => 'svg.users', 'route' => 'admin.users.index', 'title' => 'Users'],
19 | ['svg' => 'svg.log', 'route' => 'activities.index', 'title' => 'Log'],
20 | ];
21 |
22 | $this->assertSame($expected, $actual);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrustProxies.php:
--------------------------------------------------------------------------------
1 | |string|null
14 | */
15 | protected $proxies;
16 |
17 | /**
18 | * The headers that should be used to detect proxies.
19 | *
20 | * @var int
21 | */
22 | protected $headers = Request::HEADER_X_FORWARDED_FOR |
23 | Request::HEADER_X_FORWARDED_HOST |
24 | Request::HEADER_X_FORWARDED_PORT |
25 | Request::HEADER_X_FORWARDED_PROTO |
26 | Request::HEADER_X_FORWARDED_AWS_ELB;
27 | }
28 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_15_121808_create_team_user_table.php:
--------------------------------------------------------------------------------
1 | id();
13 | $table->foreignId('team_id');
14 | $table->foreignId('user_id');
15 | $table->string('role');
16 | $table->timestamps();
17 |
18 | $table->unique(['team_id', 'user_id']);
19 | });
20 | }
21 |
22 | public function down(): void
23 | {
24 | Schema::dropIfExists('team_user');
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.php:
--------------------------------------------------------------------------------
1 | > */
14 | protected $listen = [
15 | Registered::class => [
16 | SendEmailVerificationNotification::class,
17 | ],
18 | ProfileImageUploaded::class => [
19 | ResizeImageListener::class,
20 | ],
21 | ];
22 |
23 | public function boot(): void
24 | {
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in([
9 | __DIR__.'/app',
10 | __DIR__.'/config',
11 | __DIR__.'/database',
12 | __DIR__.'/resources',
13 | __DIR__.'/routes',
14 | __DIR__.'/tests',
15 | ])
16 | ->name('*.php')
17 | ->notName('*.blade.php')
18 | ->ignoreDotFiles(true)
19 | ->ignoreVCS(true);
20 |
21 | return (new Config())
22 | ->setFinder($finder)
23 | ->registerCustomFixers([
24 | new ForceFQCNFixer(),
25 | ])
26 | ->setRules([
27 | 'AdamWojs/phpdoc_force_fqcn_fixer' => true,
28 | ])
29 | ->setRiskyAllowed(true)
30 | ->setUsingCache(true);
31 |
--------------------------------------------------------------------------------
/app/Listeners/ResizeImageListener.php:
--------------------------------------------------------------------------------
1 | get("{$event->user->profile_image}");
21 |
22 | Image::make($file)
23 | ->resize(200, 200)
24 | ->save(config('filesystems.disks.profile_image.root')."/{$event->user->profile_image}");
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/EmailVerificationNotificationController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()) {
15 | return redirect()->intended(RouteServiceProvider::HOME);
16 | }
17 |
18 | $request->user()->sendEmailVerificationNotification();
19 |
20 | msg('A new verification link has been sent to the email address you provided during registration.');
21 |
22 | return back();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/Http/Requests/Account/UpdateEmailRequest.php:
--------------------------------------------------------------------------------
1 | [
20 | 'required',
21 | 'email',
22 | Rule::unique('users', 'email')->ignore($this->user()->id),
23 | ],
24 | 'current_password' => [
25 | 'required',
26 | new PasswordCheckRule($this->user()),
27 | ],
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/database/factories/ActivityFactory.php:
--------------------------------------------------------------------------------
1 | 'default',
16 | 'description' => 'here is a description',
17 | 'subject_type' => $user::class,
18 | 'event' => ActivityEvents::Deleted,
19 | 'subject_id' => $user->id,
20 | 'causer_type' => $user::class,
21 | 'causer_id' => $user->id,
22 | 'properties' => ['subject' => ['email' => 'joe.doe@example.com']],
23 | 'batch_uuid' => null,
24 | ];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_15_122036_create_team_invitations_table.php:
--------------------------------------------------------------------------------
1 | id();
13 | $table->foreignId('team_id')->constrained()->cascadeOnDelete();
14 | $table->string('email');
15 | $table->string('role')->nullable();
16 | $table->timestamps();
17 |
18 | $table->unique(['team_id', 'email']);
19 | });
20 | }
21 |
22 | public function down(): void
23 | {
24 | Schema::dropIfExists('team_invitations');
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/stubs/event.stub:
--------------------------------------------------------------------------------
1 |
24 | */
25 | public function broadcastOn(): array
26 | {
27 | return [
28 | new PrivateChannel('channel-name'),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/Http/Controllers/TeamController.php:
--------------------------------------------------------------------------------
1 | id();
13 | $table->string('uuid')->unique();
14 | $table->text('connection');
15 | $table->text('queue');
16 | $table->longText('payload');
17 | $table->longText('exception');
18 | $table->timestamp('failed_at')->useCurrent();
19 | });
20 | }
21 |
22 | public function down(): void
23 | {
24 | Schema::dropIfExists('failed_jobs');
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/database/migrations/2021_12_22_121828_add_event_column_to_activity_log_table.php:
--------------------------------------------------------------------------------
1 | table(config('activitylog.table_name'), function (Blueprint $table) {
12 | $table->string('event')->nullable()->after('subject_type');
13 | });
14 | }
15 |
16 | public function down(): void
17 | {
18 | Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
19 | $table->dropColumn('event');
20 | });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/database/migrations/2021_12_22_121829_add_batch_uuid_column_to_activity_log_table.php:
--------------------------------------------------------------------------------
1 | table(config('activitylog.table_name'), function (Blueprint $table) {
12 | $table->uuid('batch_uuid')->nullable()->after('properties');
13 | });
14 | }
15 |
16 | public function down(): void
17 | {
18 | Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) {
19 | $table->dropColumn('batch_uuid');
20 | });
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/stubs/controller.model.api.stub:
--------------------------------------------------------------------------------
1 | makeWith(AppServiceProvider::class, ['app' => $app]);
19 | Validator::shouldReceive('excludeUnvalidatedArrayKeys')->once();
20 |
21 | $appServiceProvider->boot();
22 | }
23 |
24 | /** @test */
25 | public function models_are_unguarded()
26 | {
27 | $this->assertTrue(Model::isUnguarded());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/Models/Concerns/LogsDeleteActivity.php:
--------------------------------------------------------------------------------
1 | event, static::$recordEvents)) {
25 | return;
26 | }
27 |
28 | $activity->event = ActivityEvents::Deleted->value;
29 | $activity->properties = ['subject' => $activity->subject];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/database/migrations/2021_11_09_084508_create_jobs_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
13 | $table->string('queue')->index();
14 | $table->longText('payload');
15 | $table->unsignedTinyInteger('attempts');
16 | $table->unsignedInteger('reserved_at')->nullable();
17 | $table->unsignedInteger('available_at');
18 | $table->unsignedInteger('created_at');
19 | });
20 | }
21 |
22 | public function down(): void
23 | {
24 | Schema::dropIfExists('jobs');
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "vite",
5 | "build": "vite build",
6 | "format-js": "prettier --write 'resources/**/*.{css,js,vue}'",
7 | "format-blade": "prettier --write 'resources/**/*.blade.php'"
8 | },
9 | "devDependencies": {
10 | "@hotwired/turbo": "^7.2",
11 | "@shufo/prettier-plugin-blade": "^1.0.11",
12 | "@tailwindcss/forms": "^0.5",
13 | "alpinejs": "^3.7.0",
14 | "autoprefixer": "^10.1.0",
15 | "axios": "^1.3",
16 | "laravel-echo": "^1.11.2",
17 | "laravel-vite-plugin": "^0.7.3",
18 | "lodash": "^4.17.19",
19 | "postcss": "^8.2.1",
20 | "postcss-import": "^15.1",
21 | "prettier": "^2.8",
22 | "pusher-js": "^8.0",
23 | "tailwindcss": "^3.2",
24 | "vite": "^4.0.4"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/VerifyEmailController.php:
--------------------------------------------------------------------------------
1 | user()->hasVerifiedEmail()) {
16 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
17 | }
18 |
19 | if ($request->user()->markEmailAsVerified()) {
20 | event(new Verified($request->user()));
21 | }
22 |
23 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/Providers/BladeServiceProvider.php:
--------------------------------------------------------------------------------
1 | header('Turbo-Frame') === null)
18 | { echo 'data-turbo-frame=\"_top\" target=\"_top\"'; } ?>";
19 | });
20 |
21 | Blade::directive('hasTurboFrameHeader', function () {
22 | return "header('Turbo-Frame') === null) { ?>";
23 | });
24 |
25 | Blade::directive('endHasTurboFrameHeader', function () {
26 | return '';
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/QueryBuilders/UserQueryBuilder.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class UserQueryBuilder extends Builder
14 | {
15 | use Filterable;
16 |
17 | public function search(string|null $term = null): Builder
18 | {
19 | $this->when($term, function ($query, $term) {
20 | $query->where('email', 'LIKE', "%{$term}%");
21 | });
22 |
23 | return $this;
24 | }
25 |
26 | public function role(string|null $role = null): Builder
27 | {
28 | $this->when($role, function ($query, $role) {
29 | $query->where('role', UserRoles::from($role));
30 | });
31 |
32 | return $this;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/stubs/controller.nested.singleton.api.stub:
--------------------------------------------------------------------------------
1 | user()->update(['password' => bcrypt($request->new_password)]);
21 |
22 | $request->user()->notify(new PasswordUpdatedNotification());
23 |
24 | msg_success('Your password has been successfully updated.', 'update-password');
25 |
26 | return to_route('accounts.profile');
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/Http/Middleware/RedirectIfAuthenticated.php:
--------------------------------------------------------------------------------
1 | check()) {
18 | continue;
19 | }
20 |
21 | /** @var \App\Models\User $user */
22 | $user = Auth::user();
23 |
24 | if ($user->isAdmin()) {
25 | return redirect(RouteServiceProvider::ADMIN_HOME);
26 | }
27 |
28 | return redirect(RouteServiceProvider::HOME);
29 | }
30 |
31 | return $next($request);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/stubs/markdown-notification.stub:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function via(object $notifiable): array
23 | {
24 | return ['mail'];
25 | }
26 |
27 | public function toMail(object $notifiable): MailMessage
28 | {
29 | return (new MailMessage)->markdown('{{ view }}');
30 | }
31 |
32 | /**
33 | * @return array
34 | */
35 | public function toArray(object $notifiable): array
36 | {
37 | return [
38 | //
39 | ];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Notifications/PasswordUpdatedNotification.php:
--------------------------------------------------------------------------------
1 | subject('Your password has been changed')
23 | ->greeting('Hi there')
24 | ->line('I just want to let you know that your password has been changed.')
25 | ->line('If you didn\'t changed it. Please contact us immediately.');
26 | }
27 |
28 | public function toArray(): array
29 | {
30 | return [];
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/stubs/controller.stub:
--------------------------------------------------------------------------------
1 |
4 |
5 | @php
6 | $color = session('status')['color'] ?? null;
7 | $message = session('status')['message'] ?? null;
8 | $title = session('status')['title'] ?? null;
9 | $svg = session('status')['svg'] ?? null;
10 | $undo_url = session('status')['undo_url'] ?? null;
11 | @endphp
12 |
13 | @if ($color !== null)
14 |
15 |
16 | {{ $title }}
17 |
18 |
19 |
20 | {{ $message }}
21 |
22 |
23 | @endif
24 |
25 |
--------------------------------------------------------------------------------
/stubs/mail.stub:
--------------------------------------------------------------------------------
1 |
37 | */
38 | public function attachments(): array
39 | {
40 | return [];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/config/cors.php:
--------------------------------------------------------------------------------
1 | ['api/*', 'sanctum/csrf-cookie'],
19 |
20 | 'allowed_methods' => ['*'],
21 |
22 | 'allowed_origins' => ['*'],
23 |
24 | 'allowed_origins_patterns' => [],
25 |
26 | 'allowed_headers' => ['*'],
27 |
28 | 'exposed_headers' => [],
29 |
30 | 'max_age' => 0,
31 |
32 | 'supports_credentials' => false,
33 |
34 | ];
35 |
--------------------------------------------------------------------------------
/stubs/markdown-mail.stub:
--------------------------------------------------------------------------------
1 |
37 | */
38 | public function attachments(): array
39 | {
40 | return [];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/resources/views/auth/forgot-password.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Forgot your password?
4 | No problem.
5 | Just let us know your email address and we will email
6 | you a password reset link that will allow you to choose a new one.
7 |
8 |
9 |
10 |
24 |
25 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | id();
14 | $table->foreignId('current_team_id');
15 | $table->string('email')->unique();
16 | $table->timestamp('email_verified_at')->nullable();
17 | $table->string('password');
18 | $table->string('profile_image')->nullable();
19 | $table->string('role')->default(UserRoles::Member->value);
20 | $table->rememberToken();
21 | $table->timestamps();
22 | $table->softDeletes();
23 | });
24 | }
25 |
26 | public function down(): void
27 | {
28 | Schema::dropIfExists('users');
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/PasswordResetLinkController.php:
--------------------------------------------------------------------------------
1 | only('email'));
22 |
23 | if ($status === Password::RESET_LINK_SENT) {
24 | msg('We have emailed your password reset link!');
25 |
26 | return back();
27 | }
28 |
29 | return back()->withInput($request->only('email'))
30 | ->withErrors(['email' => __($status)]);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/stubs/policy.stub:
--------------------------------------------------------------------------------
1 | get(route('dashboards.index'))
15 | ->assertMiddlewareIsApplied('auth');
16 | }
17 |
18 | /** @test */
19 | public function verified_middleware_is_applied_to_the_index_request()
20 | {
21 | $this->get(route('dashboards.index'))
22 | ->assertMiddlewareIsApplied('verified');
23 | }
24 |
25 | /** @test */
26 | public function dashboard_index_view_can_be_rendered()
27 | {
28 | $response = $this->actingAs(create_user())
29 | ->get(route('dashboards.index'));
30 |
31 | $response->assertStatus(Response::HTTP_OK)
32 | ->assertSee('Dashboard');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Http/Controllers/ActivityController.php:
--------------------------------------------------------------------------------
1 | make(Activity::class);
14 |
15 | $activities = $activityModel->with(['causer', 'subject'])
16 | ->filter($request->only(['activity_event', 'search']))
17 | ->orderBy($request->get('order_by', 'created_at'), $request->get('order_by_direction', 'desc'))
18 | ->paginate($request->get('per_page', config('castra.per_page')));
19 |
20 | return view('activities.index', [
21 | 'activities' => $activities,
22 | 'per_page' => $request->get('per_page', config('castra.per_page')),
23 | 'order_by' => $request->get('order_by', 'created_at'),
24 | 'order_by_direction' => $request->get('order_by_direction', 'desc'),
25 | ]);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/migrations/2021_12_22_121827_create_activity_log_table.php:
--------------------------------------------------------------------------------
1 | create(config('activitylog.table_name'), function (Blueprint $table) {
12 | $table->bigIncrements('id');
13 | $table->string('log_name')->nullable();
14 | $table->text('description');
15 | $table->nullableMorphs('subject', 'subject');
16 | $table->nullableMorphs('causer', 'causer');
17 | $table->json('properties')->nullable();
18 | $table->timestamps();
19 | $table->index('log_name');
20 | });
21 | }
22 |
23 | public function down(): void
24 | {
25 | Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name'));
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'domain' => env('MAILGUN_DOMAIN'),
19 | 'secret' => env('MAILGUN_SECRET'),
20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
21 | ],
22 |
23 | 'postmark' => [
24 | 'token' => env('POSTMARK_TOKEN'),
25 | ],
26 |
27 | 'ses' => [
28 | 'key' => env('AWS_ACCESS_KEY_ID'),
29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
31 | ],
32 |
33 | ];
34 |
--------------------------------------------------------------------------------
/stubs/controller.nested.api.stub:
--------------------------------------------------------------------------------
1 | authenticate();
23 |
24 | $request->session()->regenerate();
25 |
26 | return redirect()->intended(RouteServiceProvider::HOME);
27 | }
28 |
29 | public function destroy(Request $request): RedirectResponse
30 | {
31 | Auth::guard('web')->logout();
32 |
33 | $request->session()->invalidate();
34 |
35 | $request->session()->regenerateToken();
36 |
37 | return redirect('/');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/stubs/controller.model.stub:
--------------------------------------------------------------------------------
1 |
21 | */
22 | public function via(object $notifiable): array
23 | {
24 | return ['mail'];
25 | }
26 |
27 | public function toMail(object $notifiable): MailMessage
28 | {
29 | return (new MailMessage)
30 | ->line('The introduction to the notification.')
31 | ->action('Notification Action', url('/'))
32 | ->line('Thank you for using our application!');
33 | }
34 |
35 | /**
36 | * @return array
37 | */
38 | public function toArray(object $notifiable): array
39 | {
40 | return [
41 | //
42 | ];
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/licence.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Janez Cergolj
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/resources/views/components/layouts/sidebar.blade.php:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
24 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/RegisteredUserController.php:
--------------------------------------------------------------------------------
1 | $request->email,
28 | 'password' => Hash::make($request->password),
29 | ]);
30 |
31 | Auth::login($user);
32 |
33 | event(new Registered($user));
34 |
35 | return redirect(RouteServiceProvider::HOME);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Laravel
2 | APP_ENV=local
3 | APP_KEY=
4 | APP_DEBUG=true
5 | APP_URL=http://localhost
6 |
7 | LOG_CHANNEL=stack
8 | LOG_LEVEL=debug
9 |
10 | DB_CONNECTION=mysql
11 | DB_HOST=127.0.0.1
12 | DB_PORT=3306
13 | DB_DATABASE=example_app
14 | DB_USERNAME=root
15 | DB_PASSWORD=
16 |
17 | BROADCAST_DRIVER=log
18 | CACHE_DRIVER=file
19 | QUEUE_CONNECTION=sync
20 | SESSION_DRIVER=file
21 | SESSION_LIFETIME=120
22 |
23 | MEMCACHED_HOST=127.0.0.1
24 |
25 | REDIS_HOST=127.0.0.1
26 | REDIS_PASSWORD=null
27 | REDIS_PORT=6379
28 |
29 | MAIL_MAILER=smtp
30 | MAIL_HOST=mailhog
31 | MAIL_PORT=1025
32 | MAIL_USERNAME=null
33 | MAIL_PASSWORD=null
34 | MAIL_ENCRYPTION=null
35 | MAIL_FROM_ADDRESS=null
36 | MAIL_FROM_NAME="${APP_NAME}"
37 |
38 | AWS_ACCESS_KEY_ID=
39 | AWS_SECRET_ACCESS_KEY=
40 | AWS_DEFAULT_REGION=us-east-1
41 | AWS_BUCKET=
42 |
43 | PUSHER_APP_ID=
44 | PUSHER_APP_KEY=
45 | PUSHER_APP_SECRET=
46 | PUSHER_APP_CLUSTER=mt1
47 |
48 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
49 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
50 |
51 | ACTIVITY_LOGGER_DB_CONNECTION=
52 | CLOCKWORK_DATABASE_SLOW_THRESHOLD=
53 | CLOCKWORK_REQUESTS_SLOW_THRESHOLD=
54 |
--------------------------------------------------------------------------------
/app/Notifications/EmailUpdateWarningNotification.php:
--------------------------------------------------------------------------------
1 | subject('Someone requested an email address change')
27 | ->greeting('Hi there')
28 | ->line('Someone has requested an email address update.')
29 | ->line("From {$notifiable->email} to {$this->newEmail}.")
30 | ->line('Wasn\'t you?')
31 | ->line('Please contact us immediately.');
32 | }
33 |
34 | public function toArray(): array
35 | {
36 | return [];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/Unit/Notifications/PasswordUpdatedNotificationTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(ShouldQueue::class, $notification);
17 | }
18 |
19 | /** @test */
20 | public function notification_is_sent_via_email()
21 | {
22 | $notification = new PasswordUpdatedNotification;
23 |
24 | $this->assertContains('mail', $notification->via(null));
25 | }
26 |
27 | /** @test */
28 | public function notification_contains_line()
29 | {
30 | $notification = new PasswordUpdatedNotification;
31 | $this->assertStringContainsString(
32 | 'password has been changed',
33 | $notification->toMail(null)->render()
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/seeders/UserSeeder.php:
--------------------------------------------------------------------------------
1 | isProduction()) {
14 | return;
15 | }
16 |
17 | User::factory()
18 | ->create([
19 | 'email' => 'me@jcergolj.me.uk',
20 | 'email_verified_at' => now(),
21 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
22 | 'remember_token' => null,
23 | ]);
24 |
25 | User::factory()
26 | ->create([
27 | 'email' => 'admin@jcergolj.me.uk',
28 | 'email_verified_at' => now(),
29 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
30 | 'remember_token' => null,
31 | 'role' => UserRoles::Admin,
32 | ]);
33 |
34 | User::factory()
35 | ->count(50)
36 | ->create();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/resources/views/auth/verify-email.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Thanks for signing up!
4 | Before getting started, could you verify your email address
5 | by clicking on the link we just emailed to you?
6 | If you didn't receive the email, we will gladly send you another.
7 |
8 |
9 |
10 |
11 |
32 |
33 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/Unit
6 |
7 |
8 | ./tests/Feature
9 |
10 |
11 |
12 |
13 | ./app
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/phpunit.xml.bak:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./tests/Unit
6 |
7 |
8 | ./tests/Feature
9 |
10 |
11 |
12 |
13 | ./app
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/stubs/controller.nested.singleton.stub:
--------------------------------------------------------------------------------
1 | [
17 | resource_path('views'),
18 | ],
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Compiled View Path
23 | |--------------------------------------------------------------------------
24 | |
25 | | This option determines where all the compiled Blade templates will be
26 | | stored for your application. Typically, this is within the storage
27 | | directory. However, as usual, you are free to change this value.
28 | |
29 | */
30 |
31 | 'compiled' => env(
32 | 'VIEW_COMPILED_PATH',
33 | realpath(storage_path('framework/views'))
34 | ),
35 |
36 | ];
37 |
--------------------------------------------------------------------------------
/resources/views/components/table/filters.blade.php:
--------------------------------------------------------------------------------
1 | @props(['perPage', 'route' => null])
2 |
3 |
32 |
--------------------------------------------------------------------------------
/app/View/Components/Sidebar.php:
--------------------------------------------------------------------------------
1 | id === null) {
18 | $this->user = user();
19 | } else {
20 | $this->user = $user;
21 | }
22 | }
23 |
24 | public function render(): View
25 | {
26 | $this->menu = $this->menuStrategy()->build();
27 |
28 | return view('components.layouts.sidebar');
29 | }
30 |
31 | /** @throws Exception */
32 | public function menuStrategy(): mixed
33 | {
34 | $menus = [
35 | 'admin' => 'AdminNav',
36 | 'member' => 'MemberNav',
37 | ];
38 |
39 | if (! array_key_exists($this->user->role->value, $menus)) {
40 | throw new Exception('Nav class does not exists.');
41 | }
42 |
43 | $class = "App\Services\\".$menus[$this->user->role->value];
44 |
45 | return new $class();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | 1,
15 | 'email' => $this->faker->unique()->safeEmail,
16 | 'email_verified_at' => now(),
17 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
18 | 'remember_token' => Str::random(10),
19 | 'profile_image' => null,
20 | 'role' => UserRoles::Member,
21 | ];
22 | }
23 |
24 | public function unverified(): Factory
25 | {
26 | return $this->state(function (array $attributes) {
27 | return [
28 | 'email_verified_at' => null,
29 | ];
30 | });
31 | }
32 |
33 | public function admin(): Factory
34 | {
35 | return $this->state(function (array $attributes) {
36 | return [
37 | 'role' => UserRoles::Admin,
38 | ];
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Account/EmailController.php:
--------------------------------------------------------------------------------
1 | new_email}.
24 | Please check your inbox for fuhrer instruction.",
25 | 'update-email'
26 | );
27 |
28 | $request->user()?->notify(new EmailUpdateRequestNotification(
29 | Carbon::now()->addDay(),
30 | $request->new_email
31 | ));
32 |
33 | $request->user()?->notify(new EmailUpdateWarningNotification($request->new_email));
34 |
35 | return to_route('accounts.profile');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Unit/Http/Middleware/AdminTest.php:
--------------------------------------------------------------------------------
1 | makeRequestWith(create_admin());
17 |
18 | $expectedResponse = new Response('allowed', Response::HTTP_OK);
19 | $next = function () use ($expectedResponse) {
20 | return $expectedResponse;
21 | };
22 |
23 | $actualResponse = (new Admin)->handle($request, $next);
24 |
25 | $this->assertSame($expectedResponse, $actualResponse);
26 | }
27 |
28 | /** @test */
29 | public function do_not_allow_user_without_admin_role_to_continue()
30 | {
31 | $this->expectException(HttpException::class);
32 |
33 | $request = $this->makeRequestWith(create_member());
34 |
35 | $response = (new Admin)->handle($request, function () {
36 | });
37 |
38 | $response->assertStatus(Response::HTTP_FORBIDDEN);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Models/Activity.php:
--------------------------------------------------------------------------------
1 | */
18 | protected $casts = [
19 | 'id' => 'integer',
20 | 'subject_id' => 'integer',
21 | 'causer_id' => 'integer',
22 | 'properties' => 'array',
23 | 'event' => ActivityEvents::class,
24 | ];
25 |
26 | /** @param Builder $query */
27 | public function newEloquentBuilder($query): ActivityQueryBuilder
28 | {
29 | return new ActivityQueryBuilder($query);
30 | }
31 |
32 | public static function getSubjectTypes(): Collection
33 | {
34 | return self::groupBy('subject_type')->get('subject_type')->pluck('subject_type');
35 | }
36 |
37 | protected static function boot(): void
38 | {
39 | parent::boot();
40 | static::addGlobalScope(new VisibleToScope('causer_id'));
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/web.config:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/resources/views/components/table/table.blade.php:
--------------------------------------------------------------------------------
1 | @props(['bulkDeleteRoute' => null, 'items' => null])
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | @if ($bulkDeleteRoute !== null)
10 |
11 |
13 |
14 |
15 |
16 | @endif
17 |
18 | {{ $thead }}
19 |
20 |
21 |
22 |
23 |
24 | {{ $tbody }}
25 |
26 |
27 |
28 | {{ $items->withQueryString()->links() }}
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/Feature/Http/Controllers/Admin/DashboardControllerTest.php:
--------------------------------------------------------------------------------
1 | get(route('admin.dashboards.index'))
15 | ->assertMiddlewareIsApplied('auth');
16 | }
17 |
18 | /** @test */
19 | public function verified_middleware_is_applied_to_the_index_request()
20 | {
21 | $this->get(route('admin.dashboards.index'))
22 | ->assertMiddlewareIsApplied('verified');
23 | }
24 |
25 | /** @test */
26 | public function admin_middleware_is_applied_to_the_index_request()
27 | {
28 | $this->get(route('admin.dashboards.index'))
29 | ->assertMiddlewareIsApplied('admin');
30 | }
31 |
32 | /** @test */
33 | public function dashboard_index_view_can_be_rendered()
34 | {
35 | $response = $this->actingAs(create_admin())
36 | ->get(route('admin.dashboards.index'));
37 |
38 | $response->assertStatus(Response::HTTP_OK)
39 | ->assertSee('Dashboard');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/js/elements/turbo-echo-stream-tag.js:
--------------------------------------------------------------------------------
1 | import { connectStreamSource, disconnectStreamSource } from '@hotwired/turbo';
2 |
3 | const subscribeTo = (type, channel) => {
4 | if (type === 'presence') {
5 | return window.Echo.join(channel);
6 | }
7 |
8 | return window.Echo[type](channel);
9 | };
10 |
11 | class TurboEchoStreamSourceElement extends HTMLElement {
12 | async connectedCallback() {
13 | connectStreamSource(this);
14 | this.subscription = subscribeTo(this.type, this.channel).listen(
15 | '.Tonysm\\TurboLaravel\\Events\\TurboStreamBroadcast',
16 | (e) => {
17 | this.dispatchMessageEvent(e.message);
18 | }
19 | );
20 | }
21 |
22 | disconnectedCallback() {
23 | disconnectStreamSource(this);
24 | if (this.subscription) {
25 | window.Echo.leave(this.channel);
26 | this.subscription = null;
27 | }
28 | }
29 |
30 | dispatchMessageEvent(data) {
31 | const event = new MessageEvent('message', { data });
32 | return this.dispatchEvent(event);
33 | }
34 |
35 | get channel() {
36 | return this.getAttribute('channel');
37 | }
38 |
39 | get type() {
40 | return this.getAttribute('type') || 'private';
41 | }
42 | }
43 |
44 | customElements.define('turbo-echo-stream-source', TurboEchoStreamSourceElement);
45 |
--------------------------------------------------------------------------------
/stubs/controller.nested.stub:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class ActivityQueryBuilder extends Builder
14 | {
15 | use Filterable;
16 |
17 | public function event(string|null $event = null): Builder
18 | {
19 | $this->when($event, function ($query, $event) {
20 | $query->where('event', ActivityEvents::from($event));
21 | });
22 |
23 | return $this;
24 | }
25 |
26 | public function causerId(string|null $causer = null): Builder
27 | {
28 | $this->when($causer, function ($query, $causer) {
29 | $query->where('causer_id', $causer);
30 | });
31 |
32 | return $this;
33 | }
34 |
35 | public function subjectType(string|null $subjectType = null): Builder
36 | {
37 | $this->when($subjectType, function ($query, $subjectType) {
38 | $query->where('subject_type', $subjectType);
39 | });
40 |
41 | return $this;
42 | }
43 |
44 | public function logName(string|null $logName = null): Builder
45 | {
46 | $this->when($logName, function ($query, $logName) {
47 | $query->where('log_name', $logName);
48 | });
49 |
50 | return $this;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Unit/QueryBuilders/UserQueryBuilderTest.php:
--------------------------------------------------------------------------------
1 | 'joe@example.com']);
20 | $jack = create_user(['email' => 'jack@example.com']);
21 | $users = User::search('joe@example.com')->get();
22 | $this->assertCount(1, $users);
23 | $this->assertTrue($users->contains($joe));
24 | $this->assertFalse($users->contains($jack));
25 | }
26 |
27 | /** @test */
28 | public function filter_by_role()
29 | {
30 | $member = create_user(['email' => 'member@example.com']);
31 | $admin = create_admin(['email' => 'admin@example.com']);
32 |
33 | $users = User::role(UserRoles::Admin->value)->get();
34 |
35 | $this->assertCount(1, $users);
36 | $this->assertTrue($users->contains($admin));
37 | $this->assertFalse($users->contains($member));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Account/VerifyNewEmailController.php:
--------------------------------------------------------------------------------
1 | logout();
18 |
19 | abort_if(! $request->hasValidSignature(), Response::HTTP_FORBIDDEN);
20 |
21 | /** @var \App\Models\User $user */
22 | $user = User::findOrFail($request->user);
23 |
24 | $user->update([
25 | 'email' => $request->new_email,
26 | ]);
27 |
28 | $this->logActivity($request, $user);
29 |
30 | msg('Your email has been successfully updated.');
31 |
32 | return to_route('login');
33 | }
34 |
35 | private function logActivity(Request $request, User $user): void
36 | {
37 | activity()
38 | ->performedOn($user)
39 | ->causedBy($user)
40 | ->event(ActivityEvents::EmailUpdatedByUser->value)
41 | ->withProperties([
42 | 'email' => $request->new_email,
43 | ])
44 | ->log(ActivityEvents::EmailUpdatedByUser->value);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Unit/Notifications/EmailUpdateWarningNotificationTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(ShouldQueue::class, $notification);
18 | }
19 |
20 | /** @test */
21 | public function notification_is_sent_via_email()
22 | {
23 | $notification = new EmailUpdateWarningNotification('email@example.com');
24 |
25 | $this->assertContains('mail', $notification->via(null));
26 | }
27 |
28 | /** @test */
29 | public function notification_contains_original_and_new_email_address()
30 | {
31 | $notification = new EmailUpdateWarningNotification('email@example.com');
32 |
33 | $user = create_user(['email' => 'joe@example.com']);
34 |
35 | $this->assertStringContainsString(
36 | 'email@example.com',
37 | $notification->toMail($user)->render()
38 | );
39 |
40 | $this->assertStringContainsString(
41 | 'joe@example.com',
42 | $notification->toMail($user)->render()
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Account/ImageController.php:
--------------------------------------------------------------------------------
1 | user()->profile_image !== null) {
24 | Storage::disk('profile_image')
25 | ->delete("{$request->user()->profile_image}");
26 | }
27 |
28 | /** @var \Illuminate\Http\UploadedFile $profileImage */
29 | $profileImage = $request->file('profile_image');
30 | $path = $profileImage->store('/', 'profile_image');
31 |
32 | $request->user()->saveImage($path);
33 |
34 | ProfileImageUploaded::dispatch($request->user());
35 |
36 | msg_success('Your profile\'s image has been successfully updated.', 'update-image');
37 |
38 | if ($request->wantsTurboStream()) {
39 | return response()->turboStreamView('accounts.images._update_stream');
40 | }
41 |
42 | return to_route('accounts.profile');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Feature/Http/Controllers/ActivityControllerTest.php:
--------------------------------------------------------------------------------
1 | get(route('activities.index'))
19 | ->assertMiddlewareIsApplied('auth');
20 | }
21 |
22 | /** @test */
23 | public function verified_middleware_is_applied_to_the_index_request()
24 | {
25 | $this->get(route('activities.index'))
26 | ->assertMiddlewareIsApplied('verified');
27 | }
28 |
29 | /** @test */
30 | public function index_view_can_be_rendered()
31 | {
32 | $activity = Activity::factory()->create();
33 |
34 | $response = $this->actingAs(create_admin())
35 | ->get(route('activities.index'));
36 |
37 | $response->assertStatus(Response::HTTP_OK)
38 | ->assertViewHas('activities')
39 | ->assertViewHas('per_page', config('castra.per_page'))
40 | ->assertViewHas('order_by', 'created_at')
41 | ->assertViewHas('order_by_direction', 'desc')
42 | ->assertSee($activity->event->value);
43 | }
44 |
45 | // user can't see causer_id
46 |
47 | // purge activities every
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Unit/Http/Requests/Auth/PasswordResetLinkRequestTest.php:
--------------------------------------------------------------------------------
1 | createFormRequest(PasswordResetLinkRequest::class)
18 | ->validate([
19 | 'email' => 'joe@example.com',
20 | ])
21 | ->assertPasses();
22 | }
23 |
24 | /**
25 | * @test
26 | *
27 | * @dataProvider validationFailsProvider
28 | */
29 | public function test_rules_fail($name, $value, $rule)
30 | {
31 | $this->createFormRequest(PasswordResetLinkRequest::class)
32 | ->validate([$name => $value])
33 | ->assertFails([$name => $rule]);
34 | }
35 |
36 | public static function validationFailsProvider()
37 | {
38 | return [
39 | 'Test email is required' => ['email', '', 'required'],
40 | 'Test email is valid email address' => ['email', 'not-valid-email-address', 'email'],
41 | ];
42 | }
43 |
44 | /** @test */
45 | public function request_is_allowed_for_all()
46 | {
47 | $request = new PasswordResetLinkRequest();
48 | $this->assertTrue($request->authorize());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/resources/views/auth/reset-password.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
--------------------------------------------------------------------------------
/resources/views/components/dropdown.blade.php:
--------------------------------------------------------------------------------
1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
2 |
3 | @php
4 | switch ($align) {
5 | case 'left':
6 | $alignmentClasses = 'origin-top-left left-0';
7 | break;
8 | case 'top':
9 | $alignmentClasses = 'origin-top';
10 | break;
11 | case 'right':
12 | default:
13 | $alignmentClasses = 'origin-top-right right-0';
14 | break;
15 | }
16 |
17 | switch ($width) {
18 | case '48':
19 | $width = 'w-48';
20 | break;
21 | }
22 | @endphp
23 |
24 |
26 |
27 | {{ $trigger }}
28 |
29 |
30 |
38 |
39 | {{ $content }}
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/tests/Unit/Notifications/EmailUpdateRequestNotificationTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(ShouldQueue::class, $notification);
19 | }
20 |
21 | /** @test */
22 | public function notification_is_sent_via_email()
23 | {
24 | $notification = new EmailUpdateRequestNotification(Carbon::now(), 'email@example.com');
25 |
26 | $this->assertContains('mail', $notification->via(null));
27 | }
28 |
29 | /** @test */
30 | public function notification_contains_temporally_signed_url()
31 | {
32 | $notification = new EmailUpdateRequestNotification(Carbon::now(), 'email@example.com');
33 |
34 | $user = create_user();
35 |
36 | $signedUrlGenerator = new SignedUrlGenerator();
37 | $signedUrl = $signedUrlGenerator->forNewEmail($user, 'email@example.com', Carbon::now());
38 |
39 | $this->assertStringContainsString(
40 | htmlspecialchars($signedUrl),
41 | $notification->toMail($user)->render()
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Providers/RouteServiceProvider.php:
--------------------------------------------------------------------------------
1 | configureRateLimiting();
20 |
21 | $this->routes(function () {
22 | Route::prefix('api')
23 | ->middleware('api')
24 | ->namespace($this->namespace)
25 | ->group(base_path('routes/api.php'));
26 |
27 | Route::middleware('web')
28 | ->namespace($this->namespace)
29 | ->group(base_path('routes/web.php'));
30 |
31 | Route::middleware('web')
32 | ->namespace($this->namespace)
33 | ->group(base_path('routes/auth.php'));
34 |
35 | Route::middleware(['web', 'auth', 'verified', 'admin'])
36 | ->namespace($this->namespace)
37 | ->group(base_path('routes/admin.php'));
38 | });
39 | }
40 |
41 | protected function configureRateLimiting(): void
42 | {
43 | RateLimiter::for('api', function (Request $request) {
44 | return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Unit/Models/Concerns/LogsDeleteActivityTest.php:
--------------------------------------------------------------------------------
1 | actingAs($admin = create_admin());
25 |
26 | config(['activitylog.subject_returns_soft_deleted_models' => true]);
27 |
28 | $this->assertCount(0, Activity::get());
29 |
30 | Schema::create('teams', function (Blueprint $table) {
31 | $table->bigIncrements('id');
32 | });
33 |
34 | $team = Team::create();
35 |
36 | $teamId = $team->id;
37 | $team->delete();
38 |
39 | $this->assertCount(1, Activity::get());
40 |
41 | $activity = Activity::first();
42 |
43 | $this->assertSame(ActivityEvents::Deleted, $activity->event);
44 |
45 | $this->assertTrue($activity->causer->is($admin));
46 |
47 | $this->assertSame($teamId, $activity->subject_id);
48 | $this->assertSame(Team::class, $activity->subject_type);
49 | }
50 | }
51 |
52 | class Team extends Model
53 | {
54 | use LogsDeleteActivity;
55 |
56 | public $timestamps = false;
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Unit/QueryBuilders/FilterableTest.php:
--------------------------------------------------------------------------------
1 | getMockForTrait(Filterable::class, [], '', true, true, true, [
15 | 'search',
16 | ]);
17 |
18 | $filterable
19 | ->expects($this->once())
20 | ->method('search');
21 |
22 | $result = $filterable->filter(['search' => true]);
23 |
24 | $this->assertSame($result, $filterable);
25 | }
26 |
27 | /** @test */
28 | public function filter_array()
29 | {
30 | $filterable = $this->getMockForTrait(Filterable::class, [], '', true, true, true, [
31 | 'search',
32 | ]);
33 |
34 | $filterable
35 | ->expects($this->once())
36 | ->method('search');
37 |
38 | $result = $filterable->filter(['search' => [true, true]]);
39 |
40 | $this->assertSame($result, $filterable);
41 | }
42 |
43 | /** @test */
44 | public function filter_method_does_not_exist()
45 | {
46 | $filterable = $this->getMockForTrait(Filterable::class, [], '', true, true, true, [
47 | 'call',
48 | ]);
49 |
50 | $filterable
51 | ->expects($this->never())
52 | ->method('call');
53 |
54 | $result = $filterable->filter(['search' => true]);
55 |
56 | $this->assertSame($result, $filterable);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/resources/views/auth/register.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
39 |
40 |
--------------------------------------------------------------------------------
/tests/Feature/Http/Controllers/Auth/EmailVerificationPromptControllerTest.php:
--------------------------------------------------------------------------------
1 | get(route('verification.notice'))
18 | ->assertMiddlewareIsApplied('auth');
19 | }
20 |
21 | /** @test */
22 | public function email_verification_view_can_be_rendered_if_user_is_not_verified()
23 | {
24 | $user = User::factory()->unverified()->create();
25 |
26 | $response = $this->actingAs($user)
27 | ->get(route('verification.notice'));
28 |
29 | $response->assertStatus(Response::HTTP_OK)
30 | ->assertViewHasForm('id="resend_verification"', 'post', route('verification.send'));
31 |
32 | $response->assertStatus(Response::HTTP_OK)
33 | ->assertViewHasForm('id="logout"', 'delete', route('logout'));
34 | }
35 |
36 | /** @test */
37 | public function already_verified_user_is_redirected_to_home_url()
38 | {
39 | $user = User::factory()->create([
40 | 'email_verified_at' => Carbon::now(),
41 | ]);
42 |
43 | $response = $this->actingAs($user)->get(route('verification.notice'));
44 |
45 | $response->assertStatus(Response::HTTP_FOUND)
46 | ->assertRedirect(RouteServiceProvider::HOME);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/NewPasswordController.php:
--------------------------------------------------------------------------------
1 | $request]);
21 | }
22 |
23 | /** @throws ValidationException */
24 | public function store(NewPasswordRequest $request): RedirectResponse
25 | {
26 | $status = $this->resetPassword($request);
27 |
28 | if ($status === Password::PASSWORD_RESET) {
29 | msg($status);
30 |
31 | return to_route('login');
32 | }
33 |
34 | return back()->withInput($request->only('email'))
35 | ->withErrors(['email' => $status]);
36 | }
37 |
38 | private function resetPassword(Request $request): mixed
39 | {
40 | return Password::reset(
41 | $request->only('email', 'password', 'password_confirmation', 'token'),
42 | function ($user) use ($request) {
43 | $user->forceFill([
44 | 'password' => Hash::make($request->password),
45 | 'remember_token' => Str::random(60),
46 | ])->save();
47 |
48 | event(new PasswordReset($user));
49 | }
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Unit/Http/Requests/Auth/LoginRequestTest.php:
--------------------------------------------------------------------------------
1 | createFormRequest(LoginRequest::class)
18 | ->validate([
19 | 'email' => 'joe@example.com',
20 | 'password' => 'valid-password',
21 | ])
22 | ->assertPasses();
23 | }
24 |
25 | /**
26 | * @test
27 | *
28 | * @dataProvider validationFailsProvider
29 | */
30 | public function test_login_rules_fail($name, $value, $rule)
31 | {
32 | $this->createFormRequest(LoginRequest::class)
33 | ->validate([$name => $value])
34 | ->assertFails([$name => $rule]);
35 | }
36 |
37 | /** @test */
38 | public function request_is_allowed_for_all()
39 | {
40 | $request = new LoginRequest();
41 | $this->assertTrue($request->authorize());
42 | }
43 |
44 | public static function validationFailsProvider()
45 | {
46 | return [
47 | 'Test email is required' => ['email', '', 'required'],
48 | 'Test email is string' => ['email', 123, 'string'],
49 | 'Test email is valid email address' => ['email', 'not-valid-email-address', 'email'],
50 | 'Test password is required' => ['password', '', 'required'],
51 | 'Test password is string' => ['password', 123, 'string'],
52 | ];
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/config/activitylog.php:
--------------------------------------------------------------------------------
1 | env('ACTIVITY_LOGGER_ENABLED', true),
9 |
10 | /*
11 | * When the clean-command is executed, all recording activities older than
12 | * the number of days specified here will be deleted.
13 | */
14 | 'delete_records_older_than_days' => 365,
15 |
16 | /*
17 | * If no log name is passed to the activity() helper
18 | * we use this default log name.
19 | */
20 | 'default_log_name' => 'default',
21 |
22 | /*
23 | * You can specify an auth driver here that gets user models.
24 | * If this is null we'll use the default Laravel auth driver.
25 | */
26 | 'default_auth_driver' => null,
27 |
28 | /*
29 | * If set to true, the subject returns soft deleted models.
30 | */
31 | 'subject_returns_soft_deleted_models' => false,
32 |
33 | /*
34 | * This model will be used to log activity.
35 | * It should implement the Spatie\Activitylog\Contracts\Activity interface
36 | * and extend Illuminate\Database\Eloquent\Model.
37 | */
38 | 'activity_model' => \Spatie\Activitylog\Models\Activity::class,
39 |
40 | /*
41 | * This is the name of the table that will be created by the migration and
42 | * used by the Activity model shipped with this package.
43 | */
44 | 'table_name' => 'activity_log',
45 |
46 | /*
47 | * This is the database connection that will be used by the migration and
48 | * the Activity model shipped with this package. In case it's not set
49 | * Laravel's database.default will be used instead.
50 | */
51 | 'database_connection' => env('ACTIVITY_LOGGER_DB_CONNECTION'),
52 | ];
53 |
--------------------------------------------------------------------------------
/config/hashing.php:
--------------------------------------------------------------------------------
1 | 'bcrypt',
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Bcrypt Options
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may specify the configuration options that should be used when
26 | | passwords are hashed using the Bcrypt algorithm. This will allow you
27 | | to control the amount of time it takes to hash the given password.
28 | |
29 | */
30 |
31 | 'bcrypt' => [
32 | 'rounds' => env('BCRYPT_ROUNDS', 10),
33 | ],
34 |
35 | /*
36 | |--------------------------------------------------------------------------
37 | | Argon Options
38 | |--------------------------------------------------------------------------
39 | |
40 | | Here you may specify the configuration options that should be used when
41 | | passwords are hashed using the Argon algorithm. These will allow you
42 | | to control the amount of time it takes to hash the given password.
43 | |
44 | */
45 |
46 | 'argon' => [
47 | 'memory' => 1024,
48 | 'threads' => 2,
49 | 'time' => 2,
50 | ],
51 |
52 | ];
53 |
--------------------------------------------------------------------------------
/tests/Unit/Listeners/ResizeImageListenerTest.php:
--------------------------------------------------------------------------------
1 | assertListening(
24 | ProfileImageUploaded::class,
25 | ResizeImageListener::class
26 | );
27 | }
28 |
29 | /** @test */
30 | public function listener_is_queued()
31 | {
32 | $this->assertInstanceOf(ShouldQueue::class, (new ResizeImageListener()));
33 | }
34 |
35 | /** @test */
36 | public function profile_image_is_resized()
37 | {
38 | Storage::disk('profile_image')
39 | ->putFileAs('', UploadedFile::fake()->image('abc123.jpg', 1000, 1000), 'abc123.jpg');
40 |
41 | $event = new ProfileImageUploaded($user = create_user(['profile_image' => 'abc123.jpg']));
42 | $listener = new ResizeImageListener();
43 | $listener->handle($event);
44 |
45 | $imageProperties = getimagesize(config('filesystems.disks.profile_image.root')."/{$user->profile_image}");
46 |
47 | $this->assertSame(200, $imageProperties[0]);
48 | $this->assertSame(200, $imageProperties[1]);
49 |
50 | Storage::disk('profile_image')->delete("{$user->profile_image}");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/helpers.php:
--------------------------------------------------------------------------------
1 | user();
7 | }
8 | }
9 |
10 | if (! function_exists('trans_validation_attribute')) {
11 | function trans_validation_attribute(string $key): string
12 | {
13 | return trans("validation.attributes.{$key}");
14 | }
15 | }
16 |
17 | if (! function_exists('msg')) {
18 | function msg(string $message, string $level = 'success', string|null $message_bag = null): void
19 | {
20 | $look_up = [
21 | 'success' => 'sky',
22 | 'error' => 'sky',
23 | 'warning' => 'sky',
24 | 'info' => 'sky',
25 | ];
26 |
27 | session()->flash(
28 | 'status',
29 | [
30 | 'color' => $look_up[$level],
31 | 'message' => $message,
32 | 'message_bag' => $message_bag,
33 | ]
34 | );
35 | }
36 | }
37 |
38 | if (! function_exists('msg_success')) {
39 | function msg_success(string $message, string|null $message_bag = null): void
40 | {
41 | msg($message, 'success', $message_bag);
42 | }
43 | }
44 |
45 | if (! function_exists('msg_error')) {
46 | function msg_error(string $message, string|null $message_bag = null): void
47 | {
48 | msg($message, 'error', $message_bag);
49 | }
50 | }
51 |
52 | if (! function_exists('msg_info')) {
53 | function msg_info(string $message, string|null $message_bag = null): void
54 | {
55 | msg($message, 'info', $message_bag);
56 | }
57 | }
58 |
59 | if (! function_exists('msg_warning')) {
60 | function msg_warning(string $message, string|null $message_bag = null): void
61 | {
62 | msg($message, 'warning', $message_bag);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/Notifications/EmailUpdateRequestNotification.php:
--------------------------------------------------------------------------------
1 | signedUrlGenerator = app(SignedUrlGenerator::class);
28 | $this->validUntil = $validUntil;
29 | $this->newEmail = $newEmail;
30 | }
31 |
32 | public function via(): array
33 | {
34 | return ['mail'];
35 | }
36 |
37 | public function toMail($notifiable): MailMessage
38 | {
39 | $signedUrl = $this->signedUrlGenerator->forNewEmail(
40 | $notifiable,
41 | $this->newEmail,
42 | $this->validUntil
43 | );
44 |
45 | return (new MailMessage())
46 | ->subject('Please confirm your new email')
47 | ->greeting('Hi there')
48 | ->line('Looks like you requested an email address change. Please confirm it by clicking on the link below.')
49 | ->action('Confirm email change', $signedUrl)
50 | ->line('If you didn\'t request it, please let us know.');
51 | }
52 |
53 | public function toArray(): array
54 | {
55 | return [];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/resources/views/auth/login.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
42 |
43 |
--------------------------------------------------------------------------------
/tests/Unit/Http/Requests/Auth/NewPasswordRequestTest.php:
--------------------------------------------------------------------------------
1 | createFormRequest(NewPasswordRequest::class)
19 | ->validate([
20 | 'token' => 'token',
21 | 'email' => 'joe@example.com',
22 | 'password' => 'password10',
23 | ])
24 | ->assertPasses();
25 | }
26 |
27 | /**
28 | * @test
29 | *
30 | * @dataProvider validationFailsProvider
31 | */
32 | public function test_rules_fail($name, $value, $rule)
33 | {
34 | $this->createFormRequest(NewPasswordRequest::class)
35 | ->validate([$name => $value])
36 | ->assertFails([$name => $rule]);
37 | }
38 |
39 | public static function validationFailsProvider()
40 | {
41 | return [
42 | 'Test token is required' => ['token', '', 'required'],
43 | 'Test email is required' => ['email', '', 'required'],
44 | 'Test email is valid email address' => ['email', 'not-valid-email-address', 'email'],
45 | 'Test password does not have instance of password rule class' => ['password', '', PasswordRule::class],
46 | ];
47 | }
48 |
49 | /** @test */
50 | public function request_is_allowed_for_all()
51 | {
52 | $request = new NewPasswordRequest();
53 | $this->assertTrue($request->authorize());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | singleton(
30 | Illuminate\Contracts\Http\Kernel::class,
31 | App\Http\Kernel::class
32 | );
33 |
34 | $app->singleton(
35 | Illuminate\Contracts\Console\Kernel::class,
36 | App\Console\Kernel::class
37 | );
38 |
39 | $app->singleton(
40 | Illuminate\Contracts\Debug\ExceptionHandler::class,
41 | App\Exceptions\Handler::class
42 | );
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | Return The Application
47 | |--------------------------------------------------------------------------
48 | |
49 | | This script returns the application instance. The instance is given to
50 | | the calling script so we can separate the building of the instances
51 | | from the actual running of the application and sending responses.
52 | |
53 | */
54 |
55 | return $app;
56 |
--------------------------------------------------------------------------------
/resources/views/components/layouts/header.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
12 |
15 |
16 |
17 |
19 |
20 |
21 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tests/Unit/Components/SidebarTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(MemberNav::class, $component->menuStrategy());
18 | }
19 |
20 | /** @test */
21 | public function admin_nav_is_returned_from_strategy()
22 | {
23 | $component = new Sidebar(create_admin());
24 | $this->assertInstanceOf(AdminNav::class, $component->menuStrategy());
25 | }
26 |
27 | /** @test */
28 | public function member_nav_is_returned_from_strategy_for_auth_user()
29 | {
30 | $this->actingAs(create_user());
31 |
32 | $component = $this->app->make(Sidebar::class);
33 | $this->assertInstanceOf(MemberNav::class, $component->menuStrategy());
34 | }
35 |
36 | /** @test */
37 | public function assert_menu_strategy_method_is_called()
38 | {
39 | $component = $this->getMockBuilder(Sidebar::class)
40 | ->setConstructorArgs(['user' => create_member()])
41 | ->setMethods(['menuStrategy'])
42 | ->getMock();
43 |
44 | $component->expects($this->once())
45 | ->method('menuStrategy')
46 | ->willReturn(new MemberNav);
47 |
48 | $component->render();
49 | }
50 |
51 | /** @test */
52 | public function component_is_rendered()
53 | {
54 | $this->component(Sidebar::class, ['user' => create_member()])
55 | ->assertSee(route('dashboards.index'))
56 | ->assertSeeText('Dashboard');
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | make(Illuminate\Contracts\Console\Kernel::class);
34 |
35 | $status = $kernel->handle(
36 | $input = new Symfony\Component\Console\Input\ArgvInput,
37 | new Symfony\Component\Console\Output\ConsoleOutput
38 | );
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Shutdown The Application
43 | |--------------------------------------------------------------------------
44 | |
45 | | Once Artisan has finished running, we will fire off the shutdown events
46 | | so that any final work may be done by the application before we shut
47 | | down the process. This is the last thing to happen to the request.
48 | |
49 | */
50 |
51 | $kernel->terminate($input, $status);
52 |
53 | exit($status);
54 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class);
50 |
51 | $response = tap($kernel->handle(
52 | $request = Request::capture()
53 | ))->send();
54 |
55 | $kernel->terminate($request, $response);
56 |
--------------------------------------------------------------------------------
/tests/Feature/Http/Controllers/Account/ProfileControllerTest.php:
--------------------------------------------------------------------------------
1 | get(route('accounts.profile'))
15 | ->assertMiddlewareIsApplied('auth');
16 | }
17 |
18 | /** @test */
19 | public function verified_middleware_is_applied_to_view()
20 | {
21 | $this->get(route('accounts.profile'))
22 | ->assertMiddlewareIsApplied('verified');
23 | }
24 |
25 | /** @test */
26 | public function profile_view_can_be_rendered()
27 | {
28 | $response = $this->actingAs(create_user())
29 | ->get(route('accounts.profile'));
30 |
31 | $response->assertStatus(Response::HTTP_OK);
32 | }
33 |
34 | /** @test */
35 | public function turbo_frame_update_password_exists_with_a_link()
36 | {
37 | $response = $this->actingAs(create_user())
38 | ->get(route('accounts.profile'));
39 |
40 | $response->assertStatus(Response::HTTP_OK)
41 | ->assertElementHasChild(
42 | 'turbo-frame[id="frame_update_password"]',
43 | 'a[href="'.route('accounts.passwords.edit').'"]'
44 | );
45 | }
46 |
47 | /** @test */
48 | public function turbo_frame_update_email_exists_with_a_link()
49 | {
50 | $response = $this->actingAs(create_user())
51 | ->get(route('accounts.profile'));
52 |
53 | $response->assertStatus(Response::HTTP_OK)
54 | ->assertElementHasChild(
55 | 'turbo-frame[id="frame_update_email"]',
56 | 'a[href="'.route('accounts.emails.edit').'"]'
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/config/broadcasting.php:
--------------------------------------------------------------------------------
1 | env('BROADCAST_DRIVER', 'null'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Broadcast Connections
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the broadcast connections that will be used
26 | | to broadcast events to other systems or over websockets. Samples of
27 | | each available type of connection are provided inside this array.
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'pusher' => [
34 | 'driver' => 'pusher',
35 | 'key' => env('PUSHER_APP_KEY'),
36 | 'secret' => env('PUSHER_APP_SECRET'),
37 | 'app_id' => env('PUSHER_APP_ID'),
38 | 'options' => [
39 | 'cluster' => env('PUSHER_APP_CLUSTER'),
40 | 'useTLS' => true,
41 | ],
42 | ],
43 |
44 | 'ably' => [
45 | 'driver' => 'ably',
46 | 'key' => env('ABLY_KEY'),
47 | ],
48 |
49 | 'redis' => [
50 | 'driver' => 'redis',
51 | 'connection' => 'default',
52 | ],
53 |
54 | 'log' => [
55 | 'driver' => 'log',
56 | ],
57 |
58 | 'null' => [
59 | 'driver' => 'null',
60 | ],
61 |
62 | ],
63 |
64 | ];
65 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | group(function () {
16 | Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboards.index');
17 |
18 | Route::get('activities', [ActivityController::class, 'index'])->name('activities.index');
19 | Route::post('restored-items', [RestoredItemController::class, 'store'])->name('restored-item.store');
20 |
21 | Route::namespace('Account')
22 | ->prefix('account')
23 | ->name('accounts.')
24 | ->group(function () {
25 | Route::get('profile', [ProfileController::class, '__invoke'])->name('profile');
26 |
27 | Route::get('password/edit', [PasswordController::class, 'edit'])->name('passwords.edit');
28 | Route::patch('password', [PasswordController::class, 'update'])->name('passwords.update');
29 |
30 | Route::get('email/edit', [EmailController::class, 'edit'])->name('emails.edit');
31 | Route::patch('email', [EmailController::class, 'update'])->name('emails.update');
32 |
33 | Route::get('profile-image/edit', [ImageController::class, 'edit'])->name('profile-images.edit');
34 | Route::patch('profile-image', [ImageController::class, 'update'])->name('profile-images.update');
35 | });
36 | });
37 |
38 | Route::get('account/verify-email', [VerifyNewEmailController::class, '__invoke'])
39 | ->middleware('throttle:6,1')
40 | ->name('accounts.verification.verify');
41 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | */
21 | protected $hidden = [
22 | 'password',
23 | 'remember_token',
24 | ];
25 |
26 | /** @var array */
27 | protected $casts = [
28 | 'id' => 'integer',
29 | 'email_verified_at' => 'datetime',
30 | 'deleted_at' => 'datetime',
31 | 'role' => UserRoles::class,
32 | ];
33 |
34 | public function restoreRouteName(): string
35 | {
36 | return 'admin.users.restore';
37 | }
38 |
39 | /** @param Builder $query */
40 | public function newEloquentBuilder($query): UserQueryBuilder
41 | {
42 | return new UserQueryBuilder($query);
43 | }
44 |
45 | public function getProfileImageFileAttribute(): string
46 | {
47 | if ($this->profile_image === null) {
48 | return asset('images/default-user.png');
49 | }
50 |
51 | return Storage::disk('profile_image')->url("{$this->profile_image}");
52 | }
53 |
54 | public function saveImage(string $imageName): void
55 | {
56 | $this->update(['profile_image' => $imageName]);
57 | }
58 |
59 | public function isAdmin(): bool
60 | {
61 | return $this->role === UserRoles::Admin;
62 | }
63 |
64 | public function isItMe(): bool
65 | {
66 | return $this->is(user());
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Admin/UserController.php:
--------------------------------------------------------------------------------
1 | filter($request->only(['search', 'role']))
18 | ->orderBy($request->get('order_by', 'id'), $request->get('order_by_direction', 'asc'))
19 | ->paginate($request->get('per_page', config('castra.per_page')));
20 |
21 | return view('admin.users.index', [
22 | 'users' => $users,
23 | 'per_page' => $request->get('per_page', config('castra.per_page')),
24 | 'order_by' => $request->get('order_by', 'id'),
25 | 'order_by_direction' => $request->get('order_by_direction', 'asc'),
26 | ]);
27 | }
28 |
29 | public function destroy(User $user): RedirectResponse
30 | {
31 | abort_if(! $user->exists, Response::HTTP_NOT_FOUND, 'User do not exists.');
32 |
33 | if ($user->isItMe()) {
34 | msg_error('You cannot delete yourself.');
35 |
36 | return back();
37 | }
38 |
39 | $user->delete();
40 |
41 | msg_success('User has been successfully deleted.');
42 |
43 | $request = Request::create(URL::previous());
44 |
45 | $paginator = $user->select('id')->filter($request->only(['search', 'role']))
46 | ->orderBy($request->get('order_by', 'id'), $request->get('order_by_direction', 'asc'))
47 | ->paginate($request->get('per_page', config('castra.per_page')));
48 |
49 | $toPage = $paginator->lastPage() <= (int) $request->page ? $paginator->lastPage() : $request->page;
50 | $request->merge(['page' => $toPage]);
51 |
52 | return redirect($request->fullUrlWithQuery($request->all()));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/Rules/PasswordRule.php:
--------------------------------------------------------------------------------
1 | $value,
29 | $attribute.'_confirmation' => $this->confirmationValue,
30 | ], [
31 | $attribute => $this->rules(),
32 | ]);
33 |
34 | try {
35 | $validator->validate();
36 | } catch (ValidationException $exception) {
37 | $this->message = $validator->getMessageBag()->first();
38 | $this->failedRules = $validator->failed();
39 |
40 | return false;
41 | }
42 |
43 | return true;
44 | }
45 |
46 | public function message(): string
47 | {
48 | return $this->message;
49 | }
50 |
51 | public function passwordDefaultsRules(): void
52 | {
53 | Password::defaults(function () {
54 | $rule = Password::min(self::MIN_PASSWORD_LENGTH);
55 |
56 | return config('app.env') === 'production'
57 | ? $rule->mixedCase()->uncompromised()
58 | : $rule;
59 | });
60 | }
61 |
62 | protected function rules(): array
63 | {
64 | $this->passwordDefaultsRules();
65 |
66 | $rules = ['required', Password::defaults()];
67 |
68 | if ($this->confirmationValue !== null) {
69 | $rules[] = 'confirmed';
70 | }
71 |
72 | return $rules;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------