├── public ├── favicon.ico ├── robots.txt ├── images │ ├── iceburg.png │ ├── screenshot1.jpg │ ├── screenshot2.jpg │ ├── screenshot3.jpg │ ├── screenshot4.jpg │ ├── screenshot5.jpg │ ├── iceburg_resized.png │ ├── MySQL-Sample-Database-Schema.png │ └── Sharktopus.vs.Whalewolf.2015.1080p.BluRay.x265-RARBG-[rarbg.to].torrent ├── dist │ ├── assets │ │ ├── MacBook Pro.png │ │ ├── favicon.svg │ │ ├── logos │ │ │ ├── Cross.svg │ │ │ ├── CaretRight.svg │ │ │ ├── Menu.svg │ │ │ ├── Lightning.svg │ │ │ ├── Facebook.svg │ │ │ ├── Heart.svg │ │ │ ├── Star.svg │ │ │ ├── CheckedBox.svg │ │ │ ├── Youtube.svg │ │ │ ├── Instagram.svg │ │ │ └── Sun.svg │ │ ├── Underline1.svg │ │ ├── Highlight3.svg │ │ ├── Highlight2.svg │ │ ├── Highlight1.svg │ │ └── Underline2.svg │ └── script.js ├── MySQL-Sample-Database-Diagram.pdf ├── .htaccess └── index.php ├── database ├── .gitignore ├── factories │ ├── Iceburg │ │ ├── FieldFactory.php │ │ ├── FieldTypeFactory.php │ │ ├── BlankDataRecordFactory.php │ │ └── ModuleFactory.php │ ├── TeamFactory.php │ └── UserFactory.php └── migrations │ ├── 2023_02_11_182343_create_saas_table.php │ ├── 2023_01_30_050504_create_crm_statuses_table.php │ ├── 2023_02_20_224345_create_crm_themes_table.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2023_01_04_025540_create_crm_types_table.php │ ├── 2015_05_21_100000_create_teams_table.php │ ├── 2020_05_21_200000_create_team_user_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2020_05_21_300000_create_team_invitations_table.php │ ├── 2023_01_02_050520_create_sessions_table.php │ ├── 2023_02_04_052358_create_jobs_table.php │ ├── 2019_05_03_000003_create_subscription_items_table.php │ ├── 2019_05_03_000001_create_customer_columns.php │ ├── 2023_01_25_160524_create_crm_plans_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2019_05_03_000002_create_subscriptions_table.php │ ├── 2023_01_04_025708_create_crms_table.php │ ├── 2023_01_31_212727_create_connections_table.php │ ├── 2014_10_12_000000_create_users_table.php │ └── 2014_10_12_200000_add_two_factor_columns_to_users_table.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── app ├── Seeder │ └── Seed.php ├── Http │ ├── Controllers │ │ ├── RegisterController.php │ │ ├── StripeController.php │ │ ├── Controller.php │ │ └── InvoiceController.php │ └── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrustHosts.php │ │ ├── TrimStrings.php │ │ ├── Authenticate.php │ │ ├── ValidateSignature.php │ │ ├── TrustProxies.php │ │ ├── Cors.php │ │ ├── RedirectIfAuthenticated.php │ │ └── HandleInertiaRequests.php ├── Models │ ├── Iceburg │ │ ├── User_Role.php │ │ ├── FieldType.php │ │ ├── DataletType.php │ │ ├── ModuleConvertable.php │ │ ├── SubpanelField.php │ │ ├── Role.php │ │ ├── RelationshipModule.php │ │ ├── ModuleGroup.php │ │ ├── User.php │ │ ├── Setting.php │ │ └── Permission.php │ ├── saas.php │ ├── CrmPlan.php │ ├── CrmTheme.php │ ├── CrmType.php │ ├── Database.php │ ├── Server.php │ ├── Connection.php │ ├── CrmStatus.php │ ├── Membership.php │ ├── TeamInvitation.php │ ├── BillingPortal.php │ ├── Crm.php │ └── Team.php ├── Actions │ ├── Jetstream │ │ ├── DeleteTeam.php │ │ ├── UpdateTeamName.php │ │ ├── CreateTeam.php │ │ ├── DeleteUser.php │ │ └── RemoveTeamMember.php │ └── Fortify │ │ ├── PasswordValidationRules.php │ │ ├── ResetUserPassword.php │ │ ├── UpdateUserPassword.php │ │ ├── CreateNewUser.php │ │ └── UpdateUserProfileInformation.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── RouteServiceProvider.php │ └── FortifyServiceProvider.php ├── Listeners │ ├── CreateNewCRM.php │ └── StripeEventListener.php ├── Console │ ├── Kernel.php │ └── Commands │ │ └── DeleteExpiredFreeDatabases.php ├── Events │ └── NewCrm.php ├── Jobs │ └── GenerateCrm.php ├── Exceptions │ └── Handler.php ├── Mail │ └── SupportMail.php └── Policies │ └── TeamPolicy.php ├── resources ├── js │ ├── Pages │ │ ├── Payment.vue │ │ ├── Subscription.vue │ │ ├── Billing.vue │ │ ├── Teams │ │ │ ├── Create.vue │ │ │ └── Show.vue │ │ ├── API │ │ │ └── Index.vue │ │ ├── Auth │ │ │ ├── ForgotPassword.vue │ │ │ └── ConfirmPassword.vue │ │ └── Profile │ │ │ └── Show.vue │ ├── Components │ │ ├── SectionBorder.vue │ │ ├── InputError.vue │ │ ├── InputLabel.vue │ │ ├── AuthenticationCard.vue │ │ ├── ActionMessage.vue │ │ ├── ApplicationMark.vue │ │ ├── SectionTitle.vue │ │ ├── PrimaryButton.vue │ │ ├── DangerButton.vue │ │ ├── SecondaryButton.vue │ │ ├── ActionSection.vue │ │ ├── AuthenticationCardLogo.vue │ │ ├── TextInput.vue │ │ ├── DaysSince.vue │ │ ├── NavLink.vue │ │ ├── Checkbox.vue │ │ ├── DropdownLink.vue │ │ ├── ResponsiveNavLink.vue │ │ ├── DialogModal.vue │ │ ├── Alert.vue │ │ ├── FormSection.vue │ │ └── ConfirmationModal.vue │ ├── app.js │ └── bootstrap.js ├── css │ └── app.css ├── markdown │ ├── policy.md │ └── terms.md └── views │ ├── vendor │ └── jetstream │ │ ├── components │ │ ├── input-error.blade.php │ │ ├── section-border.blade.php │ │ ├── label.blade.php │ │ ├── dropdown-link.blade.php │ │ ├── checkbox.blade.php │ │ ├── input.blade.php │ │ ├── authentication-card.blade.php │ │ ├── button.blade.php │ │ ├── danger-button.blade.php │ │ ├── section-title.blade.php │ │ ├── validation-errors.blade.php │ │ ├── secondary-button.blade.php │ │ ├── application-mark.blade.php │ │ ├── action-section.blade.php │ │ ├── authentication-card-logo.blade.php │ │ ├── dialog-modal.blade.php │ │ ├── action-message.blade.php │ │ ├── nav-link.blade.php │ │ ├── responsive-nav-link.blade.php │ │ ├── switchable-team.blade.php │ │ ├── form-section.blade.php │ │ ├── confirmation-modal.blade.php │ │ ├── dropdown.blade.php │ │ ├── confirms-password.blade.php │ │ └── modal.blade.php │ │ └── mail │ │ └── team-invitation.blade.php │ ├── emails │ └── support.blade.php │ └── app.blade.php ├── postcss.config.js ├── tests ├── TestCase.php ├── Unit │ └── ExampleTest.php ├── Feature │ ├── ExampleTest.php │ ├── BrowserSessionsTest.php │ ├── CreateTeamTest.php │ ├── UpdateTeamNameTest.php │ ├── ProfileInformationTest.php │ ├── DeleteApiTokenTest.php │ ├── CreateApiTokenTest.php │ ├── LeaveTeamTest.php │ ├── DeleteTeamTest.php │ ├── AuthenticationTest.php │ ├── RemoveTeamMemberTest.php │ ├── DeleteAccountTest.php │ ├── PasswordConfirmationTest.php │ ├── ApiTokenPermissionsTest.php │ ├── UpdateTeamMemberRoleTest.php │ ├── RegistrationTest.php │ ├── InviteTeamMemberTest.php │ └── UpdatePasswordTest.php └── CreatesApplication.php ├── .gitattributes ├── .gitignore ├── .editorconfig ├── vite.config.js ├── lang └── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php ├── routes ├── channels.php ├── api.php └── console.php ├── tailwind.config.js ├── package.json ├── config ├── cors.php ├── view.php ├── services.php └── hashing.php ├── phpunit.xml ├── .env.example ├── artisan └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /app/Seeder/Seed.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /resources/js/Pages/Subscription.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/images/iceburg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/iceburg.png -------------------------------------------------------------------------------- /public/images/screenshot1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/screenshot1.jpg -------------------------------------------------------------------------------- /public/images/screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/screenshot2.jpg -------------------------------------------------------------------------------- /public/images/screenshot3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/screenshot3.jpg -------------------------------------------------------------------------------- /public/images/screenshot4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/screenshot4.jpg -------------------------------------------------------------------------------- /public/images/screenshot5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/screenshot5.jpg -------------------------------------------------------------------------------- /public/dist/assets/MacBook Pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/dist/assets/MacBook Pro.png -------------------------------------------------------------------------------- /public/images/iceburg_resized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/iceburg_resized.png -------------------------------------------------------------------------------- /resources/markdown/policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | Edit this file to define the privacy policy for your application. 4 | -------------------------------------------------------------------------------- /resources/markdown/terms.md: -------------------------------------------------------------------------------- 1 | # Terms of Service 2 | 3 | Edit this file to define the terms of service for your application. 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/MySQL-Sample-Database-Diagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/MySQL-Sample-Database-Diagram.pdf -------------------------------------------------------------------------------- /public/images/MySQL-Sample-Database-Schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/MySQL-Sample-Database-Schema.png -------------------------------------------------------------------------------- /app/Http/Controllers/RegisterController.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/input-error.blade.php: -------------------------------------------------------------------------------- 1 | @props(['for']) 2 | 3 | @error($for) 4 |

merge(['class' => 'text-sm text-red-600']) }}>{{ $message }}

5 | @enderror 6 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/section-border.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /public/images/Sharktopus.vs.Whalewolf.2015.1080p.BluRay.x265-RARBG-[rarbg.to].torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceburgcrm/iceburgsaas/HEAD/public/images/Sharktopus.vs.Whalewolf.2015.1080p.BluRay.x265-RARBG-[rarbg.to].torrent -------------------------------------------------------------------------------- /resources/js/Components/SectionBorder.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /tests/TestCase.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']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /app/Models/saas.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50']) !!}> 2 | -------------------------------------------------------------------------------- /resources/views/emails/support.blade.php: -------------------------------------------------------------------------------- 1 | 2 | # Support Message 3 | {{ auth()->user()->name }} 4 | 5 | {{ $subject }} 6 | 7 | {{ $message }} 8 | 9 | 10 | Thanks,
11 | {{ config('app.name') }} 12 |
13 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm']) !!}> 4 | -------------------------------------------------------------------------------- /app/Models/CrmStatus.php: -------------------------------------------------------------------------------- 1 | 2 | defineProps({ 3 | message: String, 4 | }); 5 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /app/Models/Iceburg/FieldType.php: -------------------------------------------------------------------------------- 1 | 2 | defineProps({ 3 | value: String, 4 | }); 5 | 6 | 7 | 13 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /app/Models/Membership.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Controllers/StripeController.php: -------------------------------------------------------------------------------- 1 | user()->redirectToBillingPortal( 12 | route('dashboard') 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/authentication-card.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ $logo }} 4 |
5 | 6 |
7 | {{ $slot }} 8 |
9 |
10 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/DeleteTeam.php: -------------------------------------------------------------------------------- 1 | purge(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/Components/AuthenticationCard.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'stripe/*', 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | merge(['type' => 'button', 'class' => 'inline-flex items-center justify-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 focus:outline-none focus:border-red-700 focus:ring focus:ring-red-200 active:bg-red-600 disabled:opacity-25 transition']) }}> 2 | {{ $slot }} 3 | 4 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/section-title.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ $title }}

4 | 5 |

6 | {{ $description }} 7 |

8 |
9 | 10 |
11 | {{ $aside ?? '' }} 12 |
13 |
14 | -------------------------------------------------------------------------------- /app/Actions/Fortify/PasswordValidationRules.php: -------------------------------------------------------------------------------- 1 | hasMany(Module::class, 'id', 'module_id'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Models/Iceburg/SubpanelField.php: -------------------------------------------------------------------------------- 1 | hasOne(Field::class, 'id', 'field_id')->with('module'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/Components/ActionMessage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/validation-errors.blade.php: -------------------------------------------------------------------------------- 1 | @if ($errors->any()) 2 |
3 |
{{ __('Whoops! Something went wrong.') }}
4 | 5 | 10 |
11 | @endif 12 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/application-mark.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /resources/js/Components/ApplicationMark.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/action-section.blade.php: -------------------------------------------------------------------------------- 1 |
merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}> 2 | 3 | {{ $title }} 4 | {{ $description }} 5 | 6 | 7 |
8 |
9 | {{ $content }} 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/authentication-card-logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/dialog-modal.blade.php: -------------------------------------------------------------------------------- 1 | @props(['id' => null, 'maxWidth' => null]) 2 | 3 | 4 |
5 |
6 | {{ $title }} 7 |
8 | 9 |
10 | {{ $content }} 11 |
12 |
13 | 14 |
15 | {{ $footer }} 16 |
17 |
18 | -------------------------------------------------------------------------------- /database/factories/Iceburg/FieldFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FieldFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition() 18 | { 19 | 20 | return [ 21 | 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/action-message.blade.php: -------------------------------------------------------------------------------- 1 | @props(['on']) 2 | 3 |
merge(['class' => 'text-sm text-gray-600']) }}> 9 | {{ $slot->isEmpty() ? 'Saved.' : $slot }} 10 |
11 | -------------------------------------------------------------------------------- /resources/js/Components/SectionTitle.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: 'resources/js/app.js', 9 | refresh: true, 10 | }), 11 | vue({ 12 | template: { 13 | transformAssetUrls: { 14 | base: null, 15 | includeAbsolute: false, 16 | }, 17 | }, 18 | }), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /app/Listeners/CreateNewCRM.php: -------------------------------------------------------------------------------- 1 | create($event->data); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/factories/Iceburg/FieldTypeFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class FieldTypeFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition() 18 | { 19 | return [ 20 | // 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/Components/PrimaryButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /database/factories/Iceburg/BlankDataRecordFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class BlankDataRecordFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition() 18 | { 19 | return [ 20 | // 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/Components/DangerButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 'fbclid', 16 | // 'utm_campaign', 17 | // 'utm_content', 18 | // 'utm_medium', 19 | // 'utm_source', 20 | // 'utm_term', 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /resources/js/Components/SecondaryButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /resources/js/Pages/Billing.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /public/dist/assets/Underline1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Models/Iceburg/Role.php: -------------------------------------------------------------------------------- 1 | belongsTo(Module::class, 'id', 'module_id'); 18 | } 19 | 20 | public function permissions(): Collection 21 | { 22 | return $this->hasMany(Permission::class, 'role_id', 'id'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Feature/BrowserSessionsTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 16 | 17 | $response = $this->delete('/user/other-browser-sessions', [ 18 | 'password' => 'password', 19 | ]); 20 | 21 | $response->assertSessionHasNoErrors(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /resources/js/Pages/Teams/Create.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/user', function (Request $request) { 18 | return $request->user(); 19 | }); 20 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /app/Listeners/StripeEventListener.php: -------------------------------------------------------------------------------- 1 | payload['type'] === 'invoice.payment_succeeded') { 19 | // Handle the incoming event... 20 | echo 'sdsdsdsdsds'; 21 | Log::emergency('test'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block pl-3 pr-4 py-2 border-l-4 border-indigo-400 text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition' 6 | : 'block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /database/factories/Iceburg/ModuleFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class ModuleFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition() 19 | { 20 | return [ 21 | 'name' => $this->faker->name(), 22 | 'table_name' => Str::random(10), 23 | 'type' => 'default', 24 | ]; 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Feature/CreateTeamTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 16 | 17 | $response = $this->post('/teams', [ 18 | 'name' => 'Test Team', 19 | ]); 20 | 21 | $this->assertCount(2, $user->fresh()->ownedTeams); 22 | $this->assertEquals('Test Team', $user->fresh()->ownedTeams()->latest('id')->first()->name); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /resources/js/Components/ActionSection.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /tests/Feature/UpdateTeamNameTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 16 | 17 | $response = $this->put('/teams/'.$user->currentTeam->id, [ 18 | 'name' => 'Test Team', 19 | ]); 20 | 21 | $this->assertCount(1, $user->fresh()->ownedTeams); 22 | $this->assertEquals('Test Team', $user->currentTeam->fresh()->name); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/TeamInvitation.php: -------------------------------------------------------------------------------- 1 | belongsTo(Jetstream::teamModel()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | TeamPolicy::class, 18 | ]; 19 | 20 | /** 21 | * Register any authentication / authorization services. 22 | * 23 | * @return void 24 | */ 25 | public function boot() 26 | { 27 | $this->registerPolicies(); 28 | 29 | // 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Models/Iceburg/RelationshipModule.php: -------------------------------------------------------------------------------- 1 | belongsTo(Relationship::class); 19 | } 20 | 21 | public function modulefields() 22 | { 23 | return $this->belongsTo(Module::class)->with('fields'); 24 | } 25 | 26 | public function module() 27 | { 28 | return $this->belongsTo(Module::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2023_02_11_182343_create_saas_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('saas'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /app/Models/Iceburg/ModuleGroup.php: -------------------------------------------------------------------------------- 1 | hasMany(Module::class); 17 | } 18 | 19 | public function getReviewAvgAttribute() 20 | { 21 | 22 | $reviews = $this->modules()->where('status', 1)->get(); 23 | $total = 0; 24 | foreach ($reviews as $review) { 25 | $total += $review->review_avg; 26 | } 27 | 28 | return $total / $reviews->count(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/factories/TeamFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->unique()->company(), 27 | 'user_id' => User::factory(), 28 | 'personal_team' => true, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/js/Components/AuthenticationCardLogo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /resources/js/Components/TextInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | -------------------------------------------------------------------------------- /tests/Feature/ProfileInformationTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 16 | 17 | $response = $this->put('/user/profile-information', [ 18 | 'name' => 'Test Name', 19 | 'email' => 'test@example.com', 20 | ]); 21 | 22 | $this->assertEquals('Test Name', $user->fresh()->name); 23 | $this->assertEquals('test@example.com', $user->fresh()->email); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/js/Components/DaysSince.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 31 | -------------------------------------------------------------------------------- /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 = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /database/migrations/2023_01_30_050504_create_crm_statuses_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name', 32); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('crm_status'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2023_02_20_224345_create_crm_themes_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name', 200); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('crm_themes'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/Actions/Fortify/ResetUserPassword.php: -------------------------------------------------------------------------------- 1 | $this->passwordRules(), 23 | ])->validate(); 24 | 25 | $user->forceFill([ 26 | 'password' => Hash::make($input['password']), 27 | ])->save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ config('app.name', 'Laravel') }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @routes 16 | @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) 17 | @inertiaHead 18 | 19 | 20 | @inertia 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 18 | $schedule->command('delete:expired_databases')->hourly(); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | * 24 | * @return void 25 | */ 26 | protected function commands() 27 | { 28 | $this->load(__DIR__.'/Commands'); 29 | 30 | require base_path('routes/console.php'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Events/NewCrm.php: -------------------------------------------------------------------------------- 1 | data = $data; 22 | } 23 | 24 | /** 25 | * Get the channels the event should broadcast on. 26 | * 27 | * @return \Illuminate\Broadcasting\Channel|array 28 | */ 29 | public function broadcastOn() 30 | { 31 | return new PrivateChannel('channel-name'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/js/Components/NavLink.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/Cors.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /resources/js/Components/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 37 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 7 | './vendor/laravel/jetstream/**/*.blade.php', 8 | './storage/framework/views/*.php', 9 | './resources/views/**/*.blade.php', 10 | './resources/js/**/*.vue', 11 | ], 12 | 13 | theme: { 14 | extend: { 15 | fontFamily: { 16 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 17 | }, 18 | }, 19 | }, 20 | daisyui: { 21 | themes: [ 22 | 23 | 'winter' 24 | 25 | 26 | ] 27 | }, 28 | 29 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography'),require("daisyui")], 30 | }; 31 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/UpdateTeamName.php: -------------------------------------------------------------------------------- 1 | authorize('update', $team); 21 | 22 | Validator::make($input, [ 23 | 'name' => ['required', 'string', 'max:255'], 24 | ])->validateWithBag('updateTeamName'); 25 | 26 | $team->forceFill([ 27 | 'name' => $input['name'], 28 | ])->save(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2023_01_04_025540_create_crm_types_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name', 100)->default(''); 19 | $table->text('description')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('crm_types'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2015_05_21_100000_create_teams_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('user_id')->index(); 19 | $table->string('name'); 20 | $table->boolean('personal_team'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('teams'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /app/Jobs/GenerateCrm.php: -------------------------------------------------------------------------------- 1 | crm = $crm; 26 | $this->user = $user; 27 | } 28 | 29 | /** 30 | * Execute the job. 31 | * 32 | * @return void 33 | */ 34 | public function handle() 35 | { 36 | $this->crm->create(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/js/Components/DropdownLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/switchable-team.blade.php: -------------------------------------------------------------------------------- 1 | @props(['team', 'component' => 'jet-dropdown-link']) 2 | 3 |
4 | @method('PUT') 5 | @csrf 6 | 7 | 8 | 9 | 10 | 11 |
12 | @if (Auth::user()->isCurrentTeam($team)) 13 | 14 | @endif 15 | 16 |
{{ $team->name }}
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /app/Models/BillingPortal.php: -------------------------------------------------------------------------------- 1 | `${title} - ${appName}`, 14 | resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), 15 | setup({ el, app, props, plugin }) { 16 | return createApp({ render: () => h(app, props) }) 17 | .use(plugin) 18 | .use(ZiggyVue, Ziggy) 19 | .mount(el); 20 | }, 21 | }); 22 | 23 | InertiaProgress.init({ color: '#4B5563' }); 24 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "vite", 5 | "build": "vite build", 6 | "watch": "vite build --watch" 7 | }, 8 | "devDependencies": { 9 | "@inertiajs/inertia": "^0.11.0", 10 | "@inertiajs/inertia-vue3": "^0.6.0", 11 | "@inertiajs/progress": "^0.2.7", 12 | "@tailwindcss/forms": "^0.5.2", 13 | "@tailwindcss/typography": "^0.5.2", 14 | "@vitejs/plugin-vue": "^4.0.0", 15 | "autoprefixer": "^10.4.7", 16 | "axios": "^1.1.2", 17 | "laravel-vite-plugin": "^0.7.2", 18 | "lodash": "^4.17.19", 19 | "postcss": "^8.4.14", 20 | "tailwindcss": "^3.1.0", 21 | "vite": "^4.0.0", 22 | "vue": "^3.2.31", 23 | "vue-stripe-js": "^1.0.1" 24 | }, 25 | "dependencies": { 26 | "@vue-stripe/vue-stripe": "^4.5.0", 27 | "daisyui": "^2.47.0", 28 | "vue-router": "^4.1.6" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2020_05_21_200000_create_team_user_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('team_id'); 19 | $table->foreignId('user_id'); 20 | $table->string('role')->nullable(); 21 | $table->timestamps(); 22 | 23 | $table->unique(['team_id', 'user_id']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('team_user'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /resources/js/Pages/API/Index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 25 | return redirect(RouteServiceProvider::HOME); 26 | } 27 | } 28 | 29 | return $next($request); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/Feature/DeleteApiTokenTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('API support is not enabled.'); 19 | } 20 | 21 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 22 | 23 | $token = $user->tokens()->create([ 24 | 'name' => 'Test Token', 25 | 'token' => Str::random(40), 26 | 'abilities' => ['create', 'read'], 27 | ]); 28 | 29 | $response = $this->delete('/user/api-tokens/'.$token->id); 30 | 31 | $this->assertCount(0, $user->fresh()->tokens); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2020_05_21_300000_create_team_invitations_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('team_id')->constrained()->cascadeOnDelete(); 19 | $table->string('email'); 20 | $table->string('role')->nullable(); 21 | $table->timestamps(); 22 | 23 | $table->unique(['team_id', 'email']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('team_invitations'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2023_01_02_050520_create_sessions_table.php: -------------------------------------------------------------------------------- 1 | string('id')->primary(); 18 | $table->foreignId('user_id')->nullable()->index(); 19 | $table->string('ip_address', 45)->nullable(); 20 | $table->text('user_agent')->nullable(); 21 | $table->longText('payload'); 22 | $table->integer('last_activity')->index(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('sessions'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /public/dist/assets/Highlight3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'current_password:web'], 23 | 'password' => $this->passwordRules(), 24 | ], [ 25 | 'current_password.current_password' => __('The provided password does not match your current password.'), 26 | ])->validateWithBag('updatePassword'); 27 | 28 | $user->forceFill([ 29 | 'password' => Hash::make($input['password']), 30 | ])->save(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2023_02_04_052358_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('queue')->index(); 19 | $table->longText('payload'); 20 | $table->unsignedTinyInteger('attempts'); 21 | $table->unsignedInteger('reserved_at')->nullable(); 22 | $table->unsignedInteger('available_at'); 23 | $table->unsignedInteger('created_at'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('jobs'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /app/Models/Crm.php: -------------------------------------------------------------------------------- 1 | 'date:Y-m-d h:i A', 15 | 'update_at' => 'date:y-m-d h:i A', 16 | ]; 17 | 18 | public function type() 19 | { 20 | return $this->hasOne(CrmType::class, 'id', 'type_id'); 21 | } 22 | 23 | public function status() 24 | { 25 | return $this->hasOne(CrmStatus::class, 'id', 'status_id'); 26 | } 27 | 28 | public static function deleteCRM($id) 29 | { 30 | $crm = Crm::where('id', $id)->first(); 31 | $status = 0; 32 | if (auth()->user()->id == 1 || $crm->user_id == auth()->user()->id) { 33 | DB::statement('DROP DATABASE '.$crm->name); 34 | $status = Crm::where('id', $id)->delete(); 35 | } 36 | 37 | return $status; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/form-section.blade.php: -------------------------------------------------------------------------------- 1 | @props(['submit']) 2 | 3 |
merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}> 4 | 5 | {{ $title }} 6 | {{ $description }} 7 | 8 | 9 |
10 |
11 |
12 |
13 | {{ $form }} 14 |
15 |
16 | 17 | @if (isset($actions)) 18 |
19 | {{ $actions }} 20 |
21 | @endif 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/mail/team-invitation.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | {{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }} 3 | 4 | @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration())) 5 | {{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }} 6 | 7 | @component('mail::button', ['url' => route('register')]) 8 | {{ __('Create Account') }} 9 | @endcomponent 10 | 11 | {{ __('If you already have an account, you may accept this invitation by clicking the button below:') }} 12 | 13 | @else 14 | {{ __('You may accept this invitation by clicking the button below:') }} 15 | @endif 16 | 17 | 18 | @component('mail::button', ['url' => $acceptUrl]) 19 | {{ __('Accept Invitation') }} 20 | @endcomponent 21 | 22 | {{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }} 23 | @endcomponent 24 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000003_create_subscription_items_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('subscription_id'); 17 | $table->string('stripe_id')->unique(); 18 | $table->string('stripe_product'); 19 | $table->string('stripe_price'); 20 | $table->integer('quantity')->nullable(); 21 | $table->timestamps(); 22 | 23 | $table->unique(['subscription_id', 'stripe_price']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('subscription_items'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/CreateTeam.php: -------------------------------------------------------------------------------- 1 | authorize('create', Jetstream::newTeamModel()); 22 | 23 | Validator::make($input, [ 24 | 'name' => ['required', 'string', 'max:255'], 25 | ])->validateWithBag('createTeam'); 26 | 27 | AddingTeam::dispatch($user); 28 | 29 | $user->switchTeam($team = $user->ownedTeams()->create([ 30 | 'name' => $input['name'], 31 | 'personal_team' => false, 32 | ])); 33 | 34 | return $team; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Models/Team.php: -------------------------------------------------------------------------------- 1 | 'boolean', 22 | ]; 23 | 24 | /** 25 | * The attributes that are mass assignable. 26 | * 27 | * @var string[] 28 | */ 29 | protected $fillable = [ 30 | 'name', 31 | 'personal_team', 32 | ]; 33 | 34 | /** 35 | * The event map for the model. 36 | * 37 | * @var array 38 | */ 39 | protected $dispatchesEvents = [ 40 | 'created' => TeamCreated::class, 41 | 'updated' => TeamUpdated::class, 42 | 'deleted' => TeamDeleted::class, 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/InvoiceController.php: -------------------------------------------------------------------------------- 1 | invoicesIncludingPending()->map(function ($invoice) { 20 | return [ 21 | 'description' => $invoice->lines->data[0]->description, 22 | 'created' => $invoice->created, 23 | 'paid' => $invoice->paid, 24 | 'status' => $invoice->status, 25 | 'url' => $invoice->hosted_invoice_url ?: null, 26 | ]; 27 | }); 28 | 29 | return Inertia::render('BillingPortal/Invoice/Index', [ 30 | 'invoices' => $invoices->toArray(), 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000001_create_customer_columns.php: -------------------------------------------------------------------------------- 1 | string('stripe_id')->nullable()->index(); 16 | $table->string('pm_type')->nullable(); 17 | $table->string('pm_last_four', 4)->nullable(); 18 | $table->timestamp('trial_ends_at')->nullable(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::table('users', function (Blueprint $table) { 28 | $table->dropColumn([ 29 | 'stripe_id', 30 | 'pm_type', 31 | 'pm_last_four', 32 | 'trial_ends_at', 33 | ]); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2023_01_25_160524_create_crm_plans_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name', 50)->default(''); 19 | $table->string('description', 200)->default(''); 20 | $table->integer('price')->default(0); 21 | $table->string('stripe_id', 64)->default(''); 22 | $table->string('logo', 64)->default(''); 23 | $table->integer('status')->default(1); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('crm_plans'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /resources/js/Components/ResponsiveNavLink.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamp('expires_at')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('personal_access_tokens'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /resources/js/Components/DialogModal.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 48 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | $request->user() ? $request->user()->subscribed('default') : false, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Feature/CreateApiTokenTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('API support is not enabled.'); 18 | } 19 | 20 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 21 | 22 | $response = $this->post('/user/api-tokens', [ 23 | 'name' => 'Test Token', 24 | 'permissions' => [ 25 | 'read', 26 | 'update', 27 | ], 28 | ]); 29 | 30 | $this->assertCount(1, $user->fresh()->tokens); 31 | $this->assertEquals('Test Token', $user->fresh()->tokens->first()->name); 32 | $this->assertTrue($user->fresh()->tokens->first()->can('read')); 33 | $this->assertFalse($user->fresh()->tokens->first()->can('delete')); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/dist/script.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | console.log(document.querySelector("#showMenu")); 3 | document 4 | .querySelector("#showMenu") 5 | .addEventListener("click", function (event) { 6 | document.querySelector("#mobileNav").classList.remove("hidden"); 7 | }); 8 | 9 | document 10 | .querySelector("#hideMenu") 11 | .addEventListener("click", function (event) { 12 | document.querySelector("#mobileNav").classList.add("hidden"); 13 | }); 14 | 15 | document.querySelectorAll("[toggleElement]").forEach((toggle) => { 16 | toggle.addEventListener("click", function (event) { 17 | console.log(toggle); 18 | const answerElement = toggle.querySelector("[answer]"); 19 | const caretElement = toggle.querySelector("img"); 20 | console.log(answerElement); 21 | if (answerElement.classList.contains("hidden")) { 22 | answerElement.classList.remove("hidden"); 23 | caretElement.classList.add("rotate-90"); 24 | } else { 25 | answerElement.classList.add("hidden"); 26 | caretElement.classList.remove("rotate-90"); 27 | } 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /database/migrations/2019_05_03_000002_create_subscriptions_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignId('user_id'); 17 | $table->string('name'); 18 | $table->string('stripe_id')->unique(); 19 | $table->string('stripe_status'); 20 | $table->string('stripe_price')->nullable(); 21 | $table->integer('quantity')->nullable(); 22 | $table->timestamp('trial_ends_at')->nullable(); 23 | $table->timestamp('ends_at')->nullable(); 24 | $table->timestamps(); 25 | 26 | $table->index(['user_id', 'stripe_status']); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | */ 33 | public function down(): void 34 | { 35 | Schema::dropIfExists('subscriptions'); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /public/dist/assets/Highlight2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/Feature/LeaveTeamTest.php: -------------------------------------------------------------------------------- 1 | withPersonalTeam()->create(); 16 | 17 | $user->currentTeam->users()->attach( 18 | $otherUser = User::factory()->create(), ['role' => 'admin'] 19 | ); 20 | 21 | $this->actingAs($otherUser); 22 | 23 | $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id); 24 | 25 | $this->assertCount(0, $user->currentTeam->fresh()->users); 26 | } 27 | 28 | public function test_team_owners_cant_leave_their_own_team() 29 | { 30 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 31 | 32 | $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$user->id); 33 | 34 | $response->assertSessionHasErrorsIn('removeTeamMember', ['team']); 35 | 36 | $this->assertNotNull($user->currentTeam->fresh()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2023_01_04_025708_create_crms_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->integer('user_id')->default(0); 19 | $table->string('name', 50)->default(''); 20 | $table->string('label', 50)->default(''); 21 | $table->string('user_crm_name', 50)->default(''); 22 | $table->integer('type_id')->default(1); 23 | $table->string('server', 32)->default('oracle4'); 24 | $table->string('plan', 32)->default(1); 25 | $table->integer('status_id')->default(1); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('crms'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /public/dist/assets/Highlight1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/Feature/DeleteTeamTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 17 | 18 | $user->ownedTeams()->save($team = Team::factory()->make([ 19 | 'personal_team' => false, 20 | ])); 21 | 22 | $team->users()->attach( 23 | $otherUser = User::factory()->create(), ['role' => 'test-role'] 24 | ); 25 | 26 | $response = $this->delete('/teams/'.$team->id); 27 | 28 | $this->assertNull($team->fresh()); 29 | $this->assertCount(0, $otherUser->fresh()->teams); 30 | } 31 | 32 | public function test_personal_teams_cant_be_deleted() 33 | { 34 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 35 | 36 | $response = $this->delete('/teams/'.$user->currentTeam->id); 37 | 38 | $this->assertNotNull($user->currentTeam->fresh()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Models/Iceburg/User.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected $fillable = [ 22 | 'name', 23 | 'email', 24 | 'password', 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * The attributes that should be cast. 39 | * 40 | * @var array 41 | */ 42 | protected $casts = [ 43 | 'email_verified_at' => 'datetime', 44 | ]; 45 | 46 | public function role() 47 | { 48 | return $this->hasOne(Role::class, 'id', 'role_id'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Feature/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 17 | 18 | $response->assertStatus(200); 19 | } 20 | 21 | public function test_users_can_authenticate_using_the_login_screen() 22 | { 23 | $user = User::factory()->create(); 24 | 25 | $response = $this->post('/login', [ 26 | 'email' => $user->email, 27 | 'password' => 'password', 28 | ]); 29 | 30 | $this->assertAuthenticated(); 31 | $response->assertRedirect(RouteServiceProvider::HOME); 32 | } 33 | 34 | public function test_users_can_not_authenticate_with_invalid_password() 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $this->post('/login', [ 39 | 'email' => $user->email, 40 | 'password' => 'wrong-password', 41 | ]); 42 | 43 | $this->assertGuest(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/migrations/2023_01_31_212727_create_connections_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->integer('user_id'); 19 | $table->string('host', 32); 20 | $table->string('database', 32); 21 | $table->string('collation', 32)->default('utf8mb4'); 22 | $table->string('charset', 32)->default('utf8mb4_unicode_ci'); 23 | $table->string('username', 32); 24 | $table->string('password', 32); 25 | $table->string('port', 8)->default('3306'); 26 | $table->integer('status')->default(1); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('connections'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , \Psr\Log\LogLevel::*> 14 | */ 15 | protected $levels = [ 16 | // 17 | ]; 18 | 19 | /** 20 | * A list of the exception types that are not reported. 21 | * 22 | * @var array> 23 | */ 24 | protected $dontReport = [ 25 | // 26 | ]; 27 | 28 | /** 29 | * A list of the inputs that are never flashed to the session on validation exceptions. 30 | * 31 | * @var array 32 | */ 33 | protected $dontFlash = [ 34 | 'current_password', 35 | 'password', 36 | 'password_confirmation', 37 | ]; 38 | 39 | /** 40 | * Register the exception handling callbacks for the application. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->reportable(function (Throwable $e) { 47 | // 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Feature/RemoveTeamMemberTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 16 | 17 | $user->currentTeam->users()->attach( 18 | $otherUser = User::factory()->create(), ['role' => 'admin'] 19 | ); 20 | 21 | $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id); 22 | 23 | $this->assertCount(0, $user->currentTeam->fresh()->users); 24 | } 25 | 26 | public function test_only_team_owner_can_remove_team_members() 27 | { 28 | $user = User::factory()->withPersonalTeam()->create(); 29 | 30 | $user->currentTeam->users()->attach( 31 | $otherUser = User::factory()->create(), ['role' => 'admin'] 32 | ); 33 | 34 | $this->actingAs($otherUser); 35 | 36 | $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$user->id); 37 | 38 | $response->assertStatus(403); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Feature/DeleteAccountTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Account deletion is not enabled.'); 18 | } 19 | 20 | $this->actingAs($user = User::factory()->create()); 21 | 22 | $response = $this->delete('/user', [ 23 | 'password' => 'password', 24 | ]); 25 | 26 | $this->assertNull($user->fresh()); 27 | } 28 | 29 | public function test_correct_password_must_be_provided_before_account_can_be_deleted() 30 | { 31 | if (! Features::hasAccountDeletionFeatures()) { 32 | return $this->markTestSkipped('Account deletion is not enabled.'); 33 | } 34 | 35 | $this->actingAs($user = User::factory()->create()); 36 | 37 | $response = $this->delete('/user', [ 38 | 'password' => 'wrong-password', 39 | ]); 40 | 41 | $this->assertNotNull($user->fresh()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/confirmation-modal.blade.php: -------------------------------------------------------------------------------- 1 | @props(['id' => null, 'maxWidth' => null]) 2 | 3 | 4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 | 12 |
13 |

14 | {{ $title }} 15 |

16 | 17 |
18 | {{ $content }} 19 |
20 |
21 |
22 |
23 | 24 |
25 | {{ $footer }} 26 |
27 |
28 | -------------------------------------------------------------------------------- /tests/Feature/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | withPersonalTeam()->create(); 16 | 17 | $response = $this->actingAs($user)->get('/user/confirm-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_password_can_be_confirmed() 23 | { 24 | $user = User::factory()->create(); 25 | 26 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 27 | 'password' => 'password', 28 | ]); 29 | 30 | $response->assertRedirect(); 31 | $response->assertSessionHasNoErrors(); 32 | } 33 | 34 | public function test_password_is_not_confirmed_with_invalid_password() 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $response->assertSessionHasErrors(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | UserCreated::class => [ 22 | SendRegisterationEmail::class, 23 | ], 24 | WebhookReceived::class => [ 25 | StripeEventListener::class, 26 | ], 27 | ]; 28 | 29 | /** 30 | * Register any events for your application. 31 | * 32 | * @return void 33 | */ 34 | public function boot() 35 | { 36 | // 37 | } 38 | 39 | /** 40 | * Determine if events and listeners should be automatically discovered. 41 | * 42 | * @return bool 43 | */ 44 | public function shouldDiscoverEvents() 45 | { 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | 15 | /** 16 | * Echo exposes an expressive API for subscribing to channels and listening 17 | * for events that are broadcast by Laravel. Echo and event broadcasting 18 | * allows your team to easily build robust real-time web applications. 19 | */ 20 | 21 | // import Echo from 'laravel-echo'; 22 | 23 | // import Pusher from 'pusher-js'; 24 | // window.Pusher = Pusher; 25 | 26 | // window.Echo = new Echo({ 27 | // broadcaster: 'pusher', 28 | // key: import.meta.env.VITE_PUSHER_APP_KEY, 29 | // wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, 30 | // wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, 31 | // wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, 32 | // forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', 33 | // enabledTransports: ['ws', 'wss'], 34 | // }); 35 | -------------------------------------------------------------------------------- /tests/Feature/ApiTokenPermissionsTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('API support is not enabled.'); 19 | } 20 | 21 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 22 | 23 | $token = $user->tokens()->create([ 24 | 'name' => 'Test Token', 25 | 'token' => Str::random(40), 26 | 'abilities' => ['create', 'read'], 27 | ]); 28 | 29 | $response = $this->put('/user/api-tokens/'.$token->id, [ 30 | 'name' => $token->name, 31 | 'permissions' => [ 32 | 'delete', 33 | 'missing-permission', 34 | ], 35 | ]); 36 | 37 | $this->assertTrue($user->fresh()->tokens->first()->can('delete')); 38 | $this->assertFalse($user->fresh()->tokens->first()->can('read')); 39 | $this->assertFalse($user->fresh()->tokens->first()->can('missing-permission')); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /resources/js/Components/Alert.vue: -------------------------------------------------------------------------------- 1 | 15 | 29 | -------------------------------------------------------------------------------- /resources/js/Components/FormSection.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/dist/assets/logos/CheckedBox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name'); 19 | $table->string('email')->unique(); 20 | $table->timestamp('email_verified_at')->nullable(); 21 | $table->string('password'); 22 | $table->integer('plan_id')->default(1); 23 | $table->string('signup_plan')->default('free'); 24 | $table->integer('first_login')->default(1); 25 | $table->integer('google_id')->default(0); 26 | $table->integer('github_id')->default(0); 27 | $table->integer('twitter_id')->default(0); 28 | $table->rememberToken(); 29 | $table->foreignId('current_team_id')->nullable(); 30 | $table->string('profile_photo_path', 2048)->nullable(); 31 | $table->timestamps(); 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::dropIfExists('users'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/DeleteUser.php: -------------------------------------------------------------------------------- 1 | deletesTeams = $deletesTeams; 26 | } 27 | 28 | /** 29 | * Delete the given user. 30 | * 31 | * @param mixed $user 32 | * @return void 33 | */ 34 | public function delete($user) 35 | { 36 | DB::transaction(function () use ($user) { 37 | $this->deleteTeams($user); 38 | $user->deleteProfilePhoto(); 39 | $user->tokens->each->delete(); 40 | $user->delete(); 41 | }); 42 | } 43 | 44 | /** 45 | * Delete the teams and team associations attached to the user. 46 | * 47 | * @param mixed $user 48 | * @return void 49 | */ 50 | protected function deleteTeams($user) 51 | { 52 | $user->teams()->detach(); 53 | 54 | $user->ownedTeams->each(function ($team) { 55 | $this->deletesTeams->delete($team); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /resources/js/Pages/Teams/Show.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 43 | -------------------------------------------------------------------------------- /tests/Feature/UpdateTeamMemberRoleTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 16 | 17 | $user->currentTeam->users()->attach( 18 | $otherUser = User::factory()->create(), ['role' => 'admin'] 19 | ); 20 | 21 | $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ 22 | 'role' => 'editor', 23 | ]); 24 | 25 | $this->assertTrue($otherUser->fresh()->hasTeamRole( 26 | $user->currentTeam->fresh(), 'editor' 27 | )); 28 | } 29 | 30 | public function test_only_team_owner_can_update_team_member_roles() 31 | { 32 | $user = User::factory()->withPersonalTeam()->create(); 33 | 34 | $user->currentTeam->users()->attach( 35 | $otherUser = User::factory()->create(), ['role' => 'admin'] 36 | ); 37 | 38 | $this->actingAs($otherUser); 39 | 40 | $response = $this->put('/teams/'.$user->currentTeam->id.'/members/'.$otherUser->id, [ 41 | 'role' => 'editor', 42 | ]); 43 | 44 | $this->assertTrue($otherUser->fresh()->hasTeamRole( 45 | $user->currentTeam->fresh(), 'admin' 46 | )); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/dist/assets/Underline2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 30 | 31 | $this->routes(function () { 32 | Route::middleware('api') 33 | ->prefix('api') 34 | ->group(base_path('routes/api.php')); 35 | 36 | Route::middleware('web') 37 | ->group(base_path('routes/web.php')); 38 | }); 39 | } 40 | 41 | /** 42 | * Configure the rate limiters for the application. 43 | * 44 | * @return void 45 | */ 46 | protected function configureRateLimiting() 47 | { 48 | RateLimiter::for('api', function (Request $request) { 49 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Console/Commands/DeleteExpiredFreeDatabases.php: -------------------------------------------------------------------------------- 1 | subDays(0); 35 | $date = date('Y-m-d H:i:s', strtotime('7 Day')); 36 | $users = User::all(); 37 | foreach ($users as $user) { 38 | if (! $user->is_subscribed()) { 39 | 40 | Crm::where('user_id', $user->id) 41 | ->where('created_at', '<=', $date)->each(function ($db) { 42 | DB::statement('DROP DATABASE '.$db->name); 43 | }); 44 | Crm::where('user_id', $user->id) 45 | ->where('created_at', '<=', $date) 46 | ->delete(); 47 | } 48 | } 49 | 50 | return Command::SUCCESS; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Providers/FortifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | email; 41 | 42 | return Limit::perMinute(5)->by($email.$request->ip()); 43 | }); 44 | 45 | RateLimiter::for('two-factor', function (Request $request) { 46 | return Limit::perMinute(5)->by($request->session()->get('login.id')); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | text('two_factor_secret') 19 | ->after('password') 20 | ->nullable(); 21 | 22 | $table->text('two_factor_recovery_codes') 23 | ->after('two_factor_secret') 24 | ->nullable(); 25 | 26 | if (Fortify::confirmsTwoFactorAuthentication()) { 27 | $table->timestamp('two_factor_confirmed_at') 28 | ->after('two_factor_recovery_codes') 29 | ->nullable(); 30 | } 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::table('users', function (Blueprint $table) { 42 | $table->dropColumn(array_merge([ 43 | 'two_factor_secret', 44 | 'two_factor_recovery_codes', 45 | ], Fortify::confirmsTwoFactorAuthentication() ? [ 46 | 'two_factor_confirmed_at', 47 | ] : [])); 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env('POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env('AWS_ACCESS_KEY_ID'), 30 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 32 | ], 33 | 'twitter' => [ 34 | 'client_id' => env('TWITTER_CLIENT_ID'), 35 | 'client_secret' => env('TWITTER_CLIENT_SECRET'), 36 | 'redirect' => '/auth/twitter/callback', 37 | ], 38 | 'twitter' => [ 39 | 'client_id' => env('GOOGLE_CLIENT_ID'), 40 | 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 41 | 'redirect' => '/auth/google/callback', 42 | ], 43 | 'github' => [ 44 | 'client_id' => env('GITHUB_CLIENT_ID'), 45 | 'client_secret' => env('GITHUB_CLIENT_SECRET'), 46 | 'redirect' => '/auth/github/callback', 47 | ], 48 | 49 | ]; 50 | -------------------------------------------------------------------------------- /app/Mail/SupportMail.php: -------------------------------------------------------------------------------- 1 | subject2 = $subject; 28 | $this->message = $message; 29 | } 30 | 31 | /** 32 | * Get the message envelope. 33 | * 34 | * @return \Illuminate\Mail\Mailables\Envelope 35 | */ 36 | public function envelope() 37 | { 38 | return new Envelope( 39 | subject: 'Support Mail', 40 | from: new Address('support@iceburgcrm.com', 'Support') 41 | ); 42 | } 43 | 44 | /** 45 | * Get the message content definition. 46 | * 47 | * @return \Illuminate\Mail\Mailables\Content 48 | */ 49 | public function content() 50 | { 51 | return new Content( 52 | markdown: 'emails.support', 53 | with: [ 54 | 'subject' => $this->subject2, 55 | 'message' => $this->message, 56 | ] 57 | ); 58 | } 59 | 60 | /** 61 | * Get the attachments for the message. 62 | * 63 | * @return array 64 | */ 65 | public function attachments() 66 | { 67 | return []; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white', 'dropdownClasses' => '']) 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 'none': 12 | case 'false': 13 | $alignmentClasses = ''; 14 | break; 15 | case 'right': 16 | default: 17 | $alignmentClasses = 'origin-top-right right-0'; 18 | break; 19 | } 20 | 21 | switch ($width) { 22 | case '48': 23 | $width = 'w-48'; 24 | break; 25 | } 26 | @endphp 27 | 28 |
29 |
30 | {{ $trigger }} 31 |
32 | 33 | 47 |
48 | -------------------------------------------------------------------------------- /app/Models/Iceburg/Setting.php: -------------------------------------------------------------------------------- 1 | pluck('value')->first(); 18 | if (! $setting && $key == 'theme') { 19 | return 'light'; 20 | } 21 | 22 | return $setting; 23 | } 24 | 25 | public static function getThemes() 26 | { 27 | return DB::table('themes')->get(); 28 | 29 | } 30 | 31 | public static function getSettings() 32 | { 33 | $data = []; 34 | Setting::all()->each(function ($item) use (&$data) { 35 | return $data[$item->name] = $item->value; 36 | }); 37 | 38 | return $data; 39 | } 40 | 41 | public static function getBreadCrumbs($level1 = null, $level2 = null, $level3 = null) 42 | { 43 | $data[] = ['name' => 'Home', 'url' => '/dashboard', 'svg' => 'home']; 44 | if ($level1 !== null) { 45 | $data[] = $level1; 46 | } 47 | if ($level2 !== null) { 48 | $data[] = $level2; 49 | } 50 | if ($level3 !== null) { 51 | $data[] = $level3; 52 | } 53 | 54 | return $data; 55 | } 56 | 57 | public static function saveSettings($setting) 58 | { 59 | foreach ($setting as $key => $value) { 60 | Setting::where('name', $key)->update(['value' => $value]); 61 | } 62 | 63 | return 1; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Feature/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Registration support is not enabled.'); 19 | } 20 | 21 | $response = $this->get('/register'); 22 | 23 | $response->assertStatus(200); 24 | } 25 | 26 | public function test_registration_screen_cannot_be_rendered_if_support_is_disabled() 27 | { 28 | if (Features::enabled(Features::registration())) { 29 | return $this->markTestSkipped('Registration support is enabled.'); 30 | } 31 | 32 | $response = $this->get('/register'); 33 | 34 | $response->assertStatus(404); 35 | } 36 | 37 | public function test_new_users_can_register() 38 | { 39 | if (! Features::enabled(Features::registration())) { 40 | return $this->markTestSkipped('Registration support is not enabled.'); 41 | } 42 | 43 | $response = $this->post('/register', [ 44 | 'name' => 'Test User', 45 | 'email' => 'test@example.com', 46 | 'password' => 'password', 47 | 'password_confirmation' => 'password', 48 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(), 49 | ]); 50 | 51 | $this->assertAuthenticated(); 52 | $response->assertRedirect(RouteServiceProvider::HOME); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /.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_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION=mysql 12 | DB_HOST=127.0.0.1 13 | DB_PORT=3306 14 | DB_DATABASE=crmbuilder 15 | DB_USERNAME=root 16 | DB_PASSWORD= 17 | 18 | DB_T_CONNECTION=tenant22 19 | DB_HOST2=127.0.0.2 20 | DB_PORT2=3306 21 | DB_ICEBURG_DATABASE=iceburg2 22 | DB_USERNAME2=root 23 | DB_PASSWORD2= 24 | 25 | STRIPE_KEY= 26 | STRIPE_SECRET= 27 | STRIPE_WEBHOOK_SECRET= 28 | 29 | STRIPE_KEY_STAGING= 30 | STRIPE_SECRET_STAGING= 31 | STRIPE_WEBHOOK_SECRET_STAGING= 32 | 33 | GITHUB_CLIENT_ID= 34 | GITHUB_CLIENT_SECRET= 35 | GOOGLE_CLIENT_ID= 36 | GOOGLE_CLIENT_SECRET= 37 | TWITTER_CLIENT_ID= 38 | TWITTER_CLIENT_SECRET= 39 | 40 | 41 | BROADCAST_DRIVER=log 42 | CACHE_DRIVER=file 43 | FILESYSTEM_DISK=local 44 | QUEUE_CONNECTION=sync 45 | SESSION_DRIVER=database 46 | SESSION_LIFETIME=120 47 | 48 | MEMCACHED_HOST=127.0.0.1 49 | 50 | REDIS_HOST=127.0.0.1 51 | REDIS_PASSWORD=null 52 | REDIS_PORT=6379 53 | 54 | MAIL_MAILER=smtp 55 | MAIL_HOST=mailhog 56 | MAIL_PORT=1025 57 | MAIL_USERNAME=null 58 | MAIL_PASSWORD=null 59 | MAIL_ENCRYPTION=null 60 | MAIL_FROM_ADDRESS="hello@example.com" 61 | MAIL_FROM_NAME="${APP_NAME}" 62 | 63 | AWS_ACCESS_KEY_ID= 64 | AWS_SECRET_ACCESS_KEY= 65 | AWS_DEFAULT_REGION=us-east-1 66 | AWS_BUCKET= 67 | AWS_USE_PATH_STYLE_ENDPOINT=false 68 | 69 | PUSHER_APP_ID= 70 | PUSHER_APP_KEY= 71 | PUSHER_APP_SECRET= 72 | PUSHER_HOST= 73 | PUSHER_PORT=443 74 | PUSHER_SCHEME=https 75 | PUSHER_APP_CLUSTER=mt1 76 | 77 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 78 | VITE_PUSHER_HOST="${PUSHER_HOST}" 79 | VITE_PUSHER_PORT="${PUSHER_PORT}" 80 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 81 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 82 | -------------------------------------------------------------------------------- /tests/Feature/InviteTeamMemberTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Team invitations not enabled.'); 20 | } 21 | 22 | Mail::fake(); 23 | 24 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 25 | 26 | $response = $this->post('/teams/'.$user->currentTeam->id.'/members', [ 27 | 'email' => 'test@example.com', 28 | 'role' => 'admin', 29 | ]); 30 | 31 | Mail::assertSent(TeamInvitation::class); 32 | 33 | $this->assertCount(1, $user->currentTeam->fresh()->teamInvitations); 34 | } 35 | 36 | public function test_team_member_invitations_can_be_cancelled() 37 | { 38 | if (! Features::sendsTeamInvitations()) { 39 | return $this->markTestSkipped('Team invitations not enabled.'); 40 | } 41 | 42 | Mail::fake(); 43 | 44 | $this->actingAs($user = User::factory()->withPersonalTeam()->create()); 45 | 46 | $invitation = $user->currentTeam->teamInvitations()->create([ 47 | 'email' => 'test@example.com', 48 | 'role' => 'admin', 49 | ]); 50 | 51 | $response = $this->delete('/team-invitations/'.$invitation->id); 52 | 53 | $this->assertCount(0, $user->currentTeam->fresh()->teamInvitations); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Feature/UpdatePasswordTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 17 | 18 | $response = $this->put('/user/password', [ 19 | 'current_password' => 'password', 20 | 'password' => 'new-password', 21 | 'password_confirmation' => 'new-password', 22 | ]); 23 | 24 | $this->assertTrue(Hash::check('new-password', $user->fresh()->password)); 25 | } 26 | 27 | public function test_current_password_must_be_correct() 28 | { 29 | $this->actingAs($user = User::factory()->create()); 30 | 31 | $response = $this->put('/user/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response->assertSessionHasErrors(); 38 | 39 | $this->assertTrue(Hash::check('password', $user->fresh()->password)); 40 | } 41 | 42 | public function test_new_passwords_must_match() 43 | { 44 | $this->actingAs($user = User::factory()->create()); 45 | 46 | $response = $this->put('/user/password', [ 47 | 'current_password' => 'password', 48 | 'password' => 'new-password', 49 | 'password_confirmation' => 'wrong-password', 50 | ]); 51 | 52 | $response->assertSessionHasErrors(); 53 | 54 | $this->assertTrue(Hash::check('password', $user->fresh()->password)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Actions/Fortify/CreateNewUser.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255'], 26 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 27 | 'password' => $this->passwordRules(), 28 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', 29 | ])->validate(); 30 | 31 | return DB::transaction(function () use ($input) { 32 | return tap(User::create([ 33 | 'name' => $input['name'], 34 | 'email' => $input['email'], 35 | 'signup_plan' => $input['signup_plan'], 36 | 'password' => Hash::make($input['password']), 37 | ]), function (User $user) { 38 | $this->createTeam($user); 39 | }); 40 | }); 41 | } 42 | 43 | /** 44 | * Create a personal team for the user. 45 | * 46 | * @return void 47 | */ 48 | protected function createTeam(User $user) 49 | { 50 | $user->ownedTeams()->save(Team::forceCreate([ 51 | 'user_id' => $user->id, 52 | 'name' => explode(' ', $user->name, 2)[0]."'s Team", 53 | 'personal_team' => true, 54 | ])); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/confirms-password.blade.php: -------------------------------------------------------------------------------- 1 | @props(['title' => __('Confirm Password'), 'content' => __('For your security, please confirm your password to continue.'), 'button' => __('Confirm')]) 2 | 3 | @php 4 | $confirmableId = md5($attributes->wire('then')); 5 | @endphp 6 | 7 | wire('then') }} 9 | x-data 10 | x-ref="span" 11 | x-on:click="$wire.startConfirmingPassword('{{ $confirmableId }}')" 12 | x-on:password-confirmed.window="setTimeout(() => $event.detail.id === '{{ $confirmableId }}' && $refs.span.dispatchEvent(new CustomEvent('then', { bubbles: false })), 250);" 13 | > 14 | {{ $slot }} 15 | 16 | 17 | @once 18 | 19 | 20 | {{ $title }} 21 | 22 | 23 | 24 | {{ $content }} 25 | 26 |
27 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 | {{ __('Cancel') }} 39 | 40 | 41 | 42 | {{ $button }} 43 | 44 | 45 |
46 | @endonce 47 | -------------------------------------------------------------------------------- /app/Models/Iceburg/Permission.php: -------------------------------------------------------------------------------- 1 | hasOne(Module::class, 'id', 'module_id') 22 | ->where('status', 1); 23 | } 24 | 25 | public function roles() 26 | { 27 | return $this->hasOne(Role::class, 'id', 'role_id'); 28 | } 29 | 30 | public static function checkPermission($module_id = 0, $type = 'read', $message = '') 31 | { 32 | 33 | if (! in_array($type, self::$types)) { 34 | return false; 35 | } 36 | if ( 37 | Auth::user()->role != 'Admin' 38 | && Module::where('id', $module_id)->value('admin') 39 | ) { 40 | return false; 41 | } 42 | Logs::insert([ 43 | 'user_id' => Auth::user()->id, 44 | 'type' => $type, 45 | 'message' => $message, 46 | 'module_id' => $module_id, 47 | 'created_at' => Carbon::now(), 48 | 'updated_at' => Carbon::now(), 49 | ]); 50 | 51 | return self::where('module_id', $module_id) 52 | ->where('role_id', Auth::user()->role_id) 53 | ->where('can_'.$type, 1)->value('id'); 54 | } 55 | 56 | public static function getPermissions($moduleId) 57 | { 58 | $permissions = []; 59 | foreach (self::$types as $type) { 60 | $permissions[$type] = self::checkPermission($moduleId, $type); 61 | } 62 | 63 | return $permissions; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserProfileInformation.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'max:255'], 22 | 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 23 | 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], 24 | ])->validateWithBag('updateProfileInformation'); 25 | 26 | if (isset($input['photo'])) { 27 | $user->updateProfilePhoto($input['photo']); 28 | } 29 | 30 | if ($input['email'] !== $user->email && 31 | $user instanceof MustVerifyEmail) { 32 | $this->updateVerifiedUser($user, $input); 33 | } else { 34 | $user->forceFill([ 35 | 'name' => $input['name'], 36 | 'email' => $input['email'], 37 | ])->save(); 38 | } 39 | } 40 | 41 | /** 42 | * Update the given verified user's profile information. 43 | * 44 | * @param mixed $user 45 | * @return void 46 | */ 47 | protected function updateVerifiedUser($user, array $input) 48 | { 49 | $user->forceFill([ 50 | 'name' => $input['name'], 51 | 'email' => $input['email'], 52 | 'email_verified_at' => null, 53 | ])->save(); 54 | 55 | $user->sendEmailVerificationNotification(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/views/vendor/jetstream/components/modal.blade.php: -------------------------------------------------------------------------------- 1 | @props(['id', 'maxWidth']) 2 | 3 | @php 4 | $id = $id ?? md5($attributes->wire('model')); 5 | 6 | $maxWidth = [ 7 | 'sm' => 'sm:max-w-sm', 8 | 'md' => 'sm:max-w-md', 9 | 'lg' => 'sm:max-w-lg', 10 | 'xl' => 'sm:max-w-xl', 11 | '2xl' => 'sm:max-w-2xl', 12 | ][$maxWidth ?? '2xl']; 13 | @endphp 14 | 15 | 44 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/RemoveTeamMember.php: -------------------------------------------------------------------------------- 1 | authorize($user, $team, $teamMember); 24 | 25 | $this->ensureUserDoesNotOwnTeam($teamMember, $team); 26 | 27 | $team->removeUser($teamMember); 28 | 29 | TeamMemberRemoved::dispatch($team, $teamMember); 30 | } 31 | 32 | /** 33 | * Authorize that the user can remove the team member. 34 | * 35 | * @param mixed $user 36 | * @param mixed $team 37 | * @param mixed $teamMember 38 | * @return void 39 | */ 40 | protected function authorize($user, $team, $teamMember) 41 | { 42 | if (! Gate::forUser($user)->check('removeTeamMember', $team) && 43 | $user->id !== $teamMember->id) { 44 | throw new AuthorizationException; 45 | } 46 | } 47 | 48 | /** 49 | * Ensure that the currently authenticated user does not own the team. 50 | * 51 | * @param mixed $teamMember 52 | * @param mixed $team 53 | * @return void 54 | */ 55 | protected function ensureUserDoesNotOwnTeam($teamMember, $team) 56 | { 57 | if ($teamMember->id === $team->owner->id) { 58 | throw ValidationException::withMessages([ 59 | 'team' => [__('You may not leave a team that you created.')], 60 | ])->errorBag('removeTeamMember'); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name(), 29 | 'email' => $this->faker->unique()->safeEmail(), 30 | 'email_verified_at' => now(), 31 | 'plan_id' => 1, 32 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 33 | 'remember_token' => Str::random(10), 34 | ]; 35 | } 36 | 37 | /** 38 | * Indicate that the model's email address should be unverified. 39 | * 40 | * @return \Illuminate\Database\Eloquent\Factories\Factory 41 | */ 42 | public function unverified() 43 | { 44 | return $this->state(function (array $attributes) { 45 | return [ 46 | 'email_verified_at' => null, 47 | ]; 48 | }); 49 | } 50 | 51 | /** 52 | * Indicate that the user should have a personal team. 53 | * 54 | * @return $this 55 | */ 56 | public function withPersonalTeam() 57 | { 58 | if (! Features::hasTeamFeatures()) { 59 | return $this->state([]); 60 | } 61 | 62 | return $this->has( 63 | Team::factory() 64 | ->state(function (array $attributes, User $user) { 65 | return ['name' => $user->name.'\'s Team', 'user_id' => $user->id, 'personal_team' => true]; 66 | }), 67 | 'ownedTeams' 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IceburgCRM SaaS 2 | =============== 3 | 4 | This is the repository for the IceburgCRM SaaS project, which powers [IceburgCRM.com](https://www.iceburgcrm.com/) and allows customers to host their own IceburgCRMs. 5 | 6 | Screenshots: 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

24 | 25 | [Demo](https://www.iceburgcrm.com) 26 | 27 | Features 28 | -------- 29 | 30 | - Host customer's IceburgCRMs 31 | - Built with Laravel JetStream, Socialite, Tailwinds, and DaisyUI 32 | - Integrates with Stripe for payment processing 33 | - Allows users to authenticate with GitHub using Socialite 34 | 35 | Requirements 36 | ------------ 37 | 38 | - PHP >= 8.1 39 | - Composer 40 | - MySQL >= 5.7 or MariaDB >= 10.2 41 | - Stripe API key 42 | - GitHub OAuth key for Socialite 43 | 44 | Installation 45 | ------------ 46 | 47 | 1. Clone this repository. 48 | 2. Copy `.env.example` to `.env` and configure the database settings, Stripe API key, and GitHub OAuth key for Socialite. 49 | 3. Run `composer install` to install the PHP dependencies. 50 | 4. Run `npm install` to install the Node.js dependencies. 51 | 5. Run `php artisan key:generate` to generate an application key. 52 | 6. Run `php artisan migrate` to run the database migrations. 53 | 7. Run `npm run dev` to compile the assets. 54 | 8. Serve the application with `php artisan serve` or use a web server like Apache or Nginx. 55 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 62 | -------------------------------------------------------------------------------- /app/Policies/TeamPolicy.php: -------------------------------------------------------------------------------- 1 | belongsToTeam($team); 31 | } 32 | 33 | /** 34 | * Determine whether the user can create models. 35 | * 36 | * @return mixed 37 | */ 38 | public function create(User $user) 39 | { 40 | return true; 41 | } 42 | 43 | /** 44 | * Determine whether the user can update the model. 45 | * 46 | * @return mixed 47 | */ 48 | public function update(User $user, Team $team) 49 | { 50 | return $user->ownsTeam($team); 51 | } 52 | 53 | /** 54 | * Determine whether the user can add team members. 55 | * 56 | * @return mixed 57 | */ 58 | public function addTeamMember(User $user, Team $team) 59 | { 60 | return $user->ownsTeam($team); 61 | } 62 | 63 | /** 64 | * Determine whether the user can update team member permissions. 65 | * 66 | * @return mixed 67 | */ 68 | public function updateTeamMember(User $user, Team $team) 69 | { 70 | return $user->ownsTeam($team); 71 | } 72 | 73 | /** 74 | * Determine whether the user can remove team members. 75 | * 76 | * @return mixed 77 | */ 78 | public function removeTeamMember(User $user, Team $team) 79 | { 80 | return $user->ownsTeam($team); 81 | } 82 | 83 | /** 84 | * Determine whether the user can delete the model. 85 | * 86 | * @return mixed 87 | */ 88 | public function delete(User $user, Team $team) 89 | { 90 | return $user->ownsTeam($team); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /resources/js/Components/ConfirmationModal.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ConfirmPassword.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 64 | -------------------------------------------------------------------------------- /public/dist/assets/logos/Sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Show.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 58 | --------------------------------------------------------------------------------