├── docs ├── .gitkeep ├── CNAME └── _config.yml ├── public ├── favicon.ico ├── robots.txt ├── index.php └── .htaccess ├── database ├── .gitignore ├── seeders │ └── DatabaseSeeder.php ├── migrations │ ├── 2025_02_17_154130_add_is_admin_to_user_table.php │ ├── 2025_03_01_000000_add_status_code_to_hits_table.php │ ├── 2025_02_26_192755_create_alerts_table.php │ ├── 2025_02_19_143139_create_usages_table.php │ ├── 2025_02_27_234235_add_location_info_to_user.php │ ├── 2025_02_19_143926_create_hits_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 2025_02_17_203210_create_personal_access_tokens_table.php │ ├── 2025_02_17_203204_add_two_factor_columns_to_users_table.php │ ├── 2025_02_17_142945_create_rates_table.php │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ └── 2025_02_19_163416_create_pulse_tables.php └── factories │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore ├── providers.php └── app.php ├── resources ├── js │ ├── app.js │ └── bootstrap.js ├── css │ └── app.css ├── views │ ├── components │ │ ├── input-error.blade.php │ │ ├── section-border.blade.php │ │ ├── label.blade.php │ │ ├── checkbox.blade.php │ │ ├── dropdown-link.blade.php │ │ ├── input.blade.php │ │ ├── authentication-card.blade.php │ │ ├── section-title.blade.php │ │ ├── danger-button.blade.php │ │ ├── application-mark.blade.php │ │ ├── validation-errors.blade.php │ │ ├── action-section.blade.php │ │ ├── authentication-card-logo.blade.php │ │ ├── action-message.blade.php │ │ ├── secondary-button.blade.php │ │ ├── button.blade.php │ │ ├── dialog-modal.blade.php │ │ ├── nav-link.blade.php │ │ ├── switchable-team.blade.php │ │ ├── form-section.blade.php │ │ ├── responsive-nav-link.blade.php │ │ ├── confirmation-modal.blade.php │ │ ├── dropdown.blade.php │ │ ├── confirms-password.blade.php │ │ ├── modal.blade.php │ │ ├── welcome.blade.php │ │ ├── application-logo.blade.php │ │ └── banner.blade.php │ ├── api │ │ └── index.blade.php │ ├── dashboard.blade.php │ ├── vendor │ │ └── pulse │ │ │ └── dashboard.blade.php │ ├── policy.blade.php │ ├── terms.blade.php │ ├── layouts │ │ ├── guest.blade.php │ │ └── app.blade.php │ ├── emails │ │ └── team-invitation.blade.php │ ├── auth │ │ ├── confirm-password.blade.php │ │ ├── forgot-password.blade.php │ │ ├── reset-password.blade.php │ │ ├── login.blade.php │ │ ├── verify-email.blade.php │ │ └── two-factor-challenge.blade.php │ ├── profile │ │ ├── show.blade.php │ │ ├── update-password-form.blade.php │ │ └── delete-user-form.blade.php │ ├── alerts │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ └── index.blade.php │ └── usages │ │ └── index.blade.php └── markdown │ ├── terms.md │ └── policy.md ├── storage ├── logs │ └── .gitignore ├── app │ ├── private │ │ └── .gitignore │ ├── public │ │ └── .gitignore │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── tests ├── Unit │ └── ExampleTest.php ├── Feature │ ├── ExampleTest.php │ ├── BrowserSessionsTest.php │ ├── AuthenticationTest.php │ ├── ProfileInformationTest.php │ ├── DeleteApiTokenTest.php │ ├── CreateApiTokenTest.php │ ├── DeleteAccountTest.php │ ├── PasswordConfirmationTest.php │ ├── RegistrationTest.php │ ├── ApiTokenPermissionsTest.php │ ├── UpdatePasswordTest.php │ ├── EmailVerificationTest.php │ ├── TwoFactorAuthenticationSettingsTest.php │ └── PasswordResetTest.php ├── TestCase.php └── Pest.php ├── postcss.config.js ├── app ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── API │ │ │ ├── v3 │ │ │ │ └── UserController.php │ │ │ └── v1 │ │ │ │ └── RateController.php │ │ ├── HitController.php │ │ ├── UsageController.php │ │ ├── AlertController.php │ │ └── RateController.php │ ├── Resources │ │ ├── v2 │ │ │ ├── RateCollection.php │ │ │ └── RateResource.php │ │ └── v3 │ │ │ ├── UserResource.php │ │ │ ├── RateCollection.php │ │ │ └── RateResource.php │ └── Middleware │ │ ├── RegisterHit.php │ │ └── DeprecationMiddleware.php ├── View │ └── Components │ │ ├── AppLayout.php │ │ └── GuestLayout.php ├── Actions │ ├── Jetstream │ │ └── DeleteUser.php │ └── Fortify │ │ ├── PasswordValidationRules.php │ │ ├── ResetUserPassword.php │ │ ├── UpdateUserPassword.php │ │ ├── CreateNewUser.php │ │ └── UpdateUserProfileInformation.php ├── Models │ ├── Usage.php │ ├── Alert.php │ ├── Hit.php │ ├── Rate.php │ └── User.php ├── Traits │ ├── AddApiMeta.php │ └── RequestProcessor.php ├── Console │ └── Commands │ │ └── PruneStats.php ├── Providers │ ├── JetstreamServiceProvider.php │ ├── HorizonServiceProvider.php │ ├── AppServiceProvider.php │ └── FortifyServiceProvider.php ├── Policies │ ├── AlertPolicy.php │ ├── HitPolicy.php │ ├── RatePolicy.php │ └── UsagePolicy.php └── Jobs │ └── ProcessHits.php ├── .gitattributes ├── routes ├── console.php ├── web.php └── api.php ├── vite.config.js ├── .editorconfig ├── artisan ├── .gitignore ├── package.json ├── tailwind.config.js ├── config ├── cors.php ├── services.php ├── api.php ├── filesystems.php ├── jetstream.php ├── sanctum.php ├── cache.php ├── mail.php ├── queue.php └── auth.php ├── README.md ├── phpunit.xml ├── .env.example └── composer.json /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | salestaxapi.ca -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /storage/app/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !private/ 3 | !public/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | [x-cloak] { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'text-sm text-red-600 dark:text-red-400']) }}>{{ $message }}

5 | @enderror 6 | -------------------------------------------------------------------------------- /resources/views/components/section-border.blade.php: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/views/components/label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: salestaxapi.ca 2 | description: Here's an API to get canadian sales tax informations. Simple as that. 3 | google_analytics: 4 | show_downloads: false 5 | theme: jekyll-theme-cayman 6 | 7 | gems: 8 | - jekyll-mentions 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 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 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800']) !!}> 2 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote'); 9 | 10 | Schedule::command('app:prune-stats')->daily(); 11 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | laravel({ 7 | input: ['resources/css/app.css', 'resources/js/app.js'], 8 | refresh: true, 9 | }), 10 | ], 11 | }); 12 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /resources/views/components/input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm']) !!}> 4 | -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | 2 | {{--
3 | {{ $logo }} 4 |
--}} 5 | 6 |
7 | {{ $slot }} 8 |
9 | 10 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /storage/pail 8 | /vendor 9 | .env 10 | .env.backup 11 | .env.production 12 | .phpactor.json 13 | .phpunit.result.cache 14 | Homestead.json 15 | Homestead.yaml 16 | npm-debug.log 17 | yarn-error.log 18 | /auth.json 19 | /.fleet 20 | /.idea 21 | /.nova 22 | /.vscode 23 | /.zed 24 | .DS_Store 25 | Todo.md 26 | -------------------------------------------------------------------------------- /resources/views/api/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('API Tokens') }} 5 |

6 |
7 | 8 |
9 |
10 | @livewire('api.api-token-manager') 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /app/Http/Controllers/API/v3/UserController.php: -------------------------------------------------------------------------------- 1 | user()->tokenCan('read'); 15 | 16 | return new UserResource($request->user()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/views/components/section-title.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ $title }}

4 | 5 |

6 | {{ $description }} 7 |

8 |
9 | 10 |
11 | {{ $aside ?? '' }} 12 |
13 |
14 | -------------------------------------------------------------------------------- /app/Actions/Jetstream/DeleteUser.php: -------------------------------------------------------------------------------- 1 | deleteProfilePhoto(); 16 | $user->tokens->each->delete(); 17 | $user->delete(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/components/danger-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/application-mark.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /resources/views/components/validation-errors.blade.php: -------------------------------------------------------------------------------- 1 | @if ($errors->any()) 2 |
3 |
{{ __('Whoops! Something went wrong.') }}
4 | 5 | 10 |
11 | @endif 12 | -------------------------------------------------------------------------------- /app/Models/Usage.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Feature/BrowserSessionsTest.php: -------------------------------------------------------------------------------- 1 | actingAs(User::factory()->create()); 9 | 10 | Livewire::test(LogoutOtherBrowserSessionsForm::class) 11 | ->set('password', 'password') 12 | ->call('logoutOtherBrowserSessions') 13 | ->assertSuccessful(); 14 | }); 15 | -------------------------------------------------------------------------------- /app/Traits/AddApiMeta.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'timestamp' => now()->toJSON(), 15 | 'version' => 3, 16 | 'alerts' => Alert::where('active', true)->get(["type", "message", "created_at"]), 17 | ], 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/views/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 | -------------------------------------------------------------------------------- /app/Actions/Fortify/PasswordValidationRules.php: -------------------------------------------------------------------------------- 1 | |string> 13 | */ 14 | protected function passwordRules(): array 15 | { 16 | return ['required', 'string', Password::default(), 'confirmed']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/views/components/authentication-card-logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /resources/views/components/action-message.blade.php: -------------------------------------------------------------------------------- 1 | @props(['on']) 2 | 3 |
merge(['class' => 'text-sm text-gray-600 dark:text-gray-400']) }}> 9 | {{ $slot->isEmpty() ? 'Saved.' : $slot }} 10 |
11 | -------------------------------------------------------------------------------- /resources/views/components/secondary-button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Dashboard') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /resources/views/vendor/pulse/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite" 7 | }, 8 | "devDependencies": { 9 | "@tailwindcss/forms": "^0.5.7", 10 | "@tailwindcss/typography": "^0.5.10", 11 | "autoprefixer": "^10.4.16", 12 | "axios": "^1.7.4", 13 | "concurrently": "^9.0.1", 14 | "laravel-vite-plugin": "^1.2.0", 15 | "postcss": "^8.4.32", 16 | "tailwindcss": "^3.4.0", 17 | "vite": "^6.0.11" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/views/policy.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{--
5 | 6 |
--}} 7 | 8 |
9 | {!! $policy !!} 10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /resources/views/terms.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | {{--
5 | 6 |
--}} 7 | 8 |
9 | {!! $terms !!} 10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | User::factory()->create([ 19 | 'name' => 'Test User', 20 | 'email' => 'test@example.com', 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/Http/Resources/v2/RateCollection.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return parent::toArray($request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Models/Alert.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $fillable = [ 15 | 'type', 16 | 'message', 17 | 'active', 18 | ]; 19 | 20 | /** 21 | * The attributes that should be cast to native types. 22 | * 23 | * @var array 24 | */ 25 | protected $casts = [ 26 | 'active' => 'boolean', 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/Hit.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/views/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 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | import typography from '@tailwindcss/typography'; 4 | 5 | /** @type {import('tailwindcss').Config} */ 6 | export default { 7 | content: [ 8 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 9 | './vendor/laravel/jetstream/**/*.blade.php', 10 | './storage/framework/views/*.php', 11 | './resources/views/**/*.blade.php', 12 | ], 13 | 14 | theme: { 15 | extend: { 16 | fontFamily: { 17 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 18 | }, 19 | }, 20 | }, 21 | 22 | plugins: [forms, typography], 23 | }; 24 | -------------------------------------------------------------------------------- /database/migrations/2025_02_17_154130_add_is_admin_to_user_table.php: -------------------------------------------------------------------------------- 1 | boolean('is_admin')->after('name')->default(false); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('users', function (Blueprint $table) { 25 | $table->dropColumn('is_admin'); 26 | }); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /app/Console/Commands/PruneStats.php: -------------------------------------------------------------------------------- 1 | subMonth(config('api.retention.hits')))->delete(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2025_03_01_000000_add_status_code_to_hits_table.php: -------------------------------------------------------------------------------- 1 | unsignedSmallInteger('status_code')->nullable()->after('user_agent'); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | */ 22 | public function down(): void 23 | { 24 | Schema::table('hits', function (Blueprint $table) { 25 | $table->dropColumn('status_code'); 26 | }); 27 | } 28 | }; -------------------------------------------------------------------------------- /app/Http/Resources/v3/UserResource.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function toArray(Request $request): array 20 | { 21 | return parent::toArray($request); 22 | } 23 | 24 | /** 25 | * Get additional data that should be returned with the resource array. 26 | * 27 | * @return array 28 | */ 29 | 30 | public function with(Request $request): array 31 | { 32 | return $this->generateMeta(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 10 | web: __DIR__ . '/../routes/web.php', 11 | api: __DIR__ . '/../routes/api.php', 12 | apiPrefix: '', 13 | commands: __DIR__ . '/../routes/console.php', 14 | health: '/up', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware) { 17 | $middleware->api(append: [ 18 | RegisterHit::class, 19 | ]); 20 | $middleware->throttleWithRedis(); 21 | }) 22 | ->withExceptions(function (Exceptions $exceptions) { 23 | // 24 | })->create(); 25 | -------------------------------------------------------------------------------- /database/migrations/2025_02_26_192755_create_alerts_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('type'); 17 | $table->string('message'); 18 | $table->boolean('active')->default(true); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('alerts'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /resources/views/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 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' 6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /app/Actions/Fortify/ResetUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function reset(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'password' => $this->passwordRules(), 23 | ])->validate(); 24 | 25 | $user->forceFill([ 26 | 'password' => Hash::make($input['password']), 27 | ])->save(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | # Handle X-XSRF-Token Header 13 | RewriteCond %{HTTP:x-xsrf-token} . 14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] 15 | 16 | # Redirect Trailing Slashes If Not A Folder... 17 | RewriteCond %{REQUEST_FILENAME} !-d 18 | RewriteCond %{REQUEST_URI} (.+)/$ 19 | RewriteRule ^ %1 [L,R=301] 20 | 21 | # Send Requests To Front Controller... 22 | RewriteCond %{REQUEST_FILENAME} !-d 23 | RewriteCond %{REQUEST_FILENAME} !-f 24 | RewriteRule ^ index.php [L] 25 | 26 | -------------------------------------------------------------------------------- /database/migrations/2025_02_19_143139_create_usages_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('version', 4); 17 | $table->string('endpoint'); 18 | $table->unsignedBigInteger('count')->default(0); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists('usages'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /app/Http/Resources/v3/RateCollection.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function toArray(Request $request): array 20 | { 21 | return parent::toArray($request); 22 | } 23 | 24 | /** 25 | * Get additional data that should be returned with the resource array. 26 | * 27 | * @return array 28 | */ 29 | 30 | public function with(Request $request): array 31 | { 32 | return $this->generateMeta(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2025_02_27_234235_add_location_info_to_user.php: -------------------------------------------------------------------------------- 1 | string('location')->after('email'); 16 | $table->string('info')->after('location')->nullable(); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | */ 23 | public function down(): void 24 | { 25 | Schema::table('users', function (Blueprint $table) { 26 | $table->dropColumn('location'); 27 | $table->dropColumn('info'); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /tests/Feature/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users cannot authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 18 | Route::get('/dashboard', function () { 19 | return view('dashboard'); 20 | })->name('dashboard'); 21 | 22 | Route::resource('rates', RateController::class)->except(['show', 'destroy']); 23 | Route::resource('usages', UsageController::class)->only(['index']); 24 | Route::resource('hits', HitController::class)->only(['index']); 25 | Route::resource('alerts', AlertController::class)->only(['index', 'create', 'store', 'edit', 'update']); 26 | }); 27 | -------------------------------------------------------------------------------- /database/migrations/2025_02_19_143926_create_hits_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->foreignIdFor(\App\Models\User::class)->nullable(); 17 | $table->string('version', 4); 18 | $table->string('endpoint'); 19 | $table->ipAddress('client')->nullable(); 20 | $table->string('user_agent')->nullable(); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | */ 28 | public function down(): void 29 | { 30 | Schema::dropIfExists('hits'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 | @livewireStyles 19 | 20 | 21 |
22 | {{ $slot }} 23 |
24 | 25 | @livewireScripts 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/Feature/ProfileInformationTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 9 | 10 | $component = Livewire::test(UpdateProfileInformationForm::class); 11 | 12 | expect($component->state['name'])->toEqual($user->name); 13 | expect($component->state['email'])->toEqual($user->email); 14 | }); 15 | 16 | test('profile information can be updated', function () { 17 | $this->actingAs($user = User::factory()->create()); 18 | 19 | Livewire::test(UpdateProfileInformationForm::class) 20 | ->set('state', ['name' => 'Test Name', 'email' => 'test@example.com']) 21 | ->call('updateProfileInformation'); 22 | 23 | expect($user->fresh()) 24 | ->name->toEqual('Test Name') 25 | ->email->toEqual('test@example.com'); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/Feature/DeleteApiTokenTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 12 | } else { 13 | $this->actingAs($user = User::factory()->create()); 14 | } 15 | 16 | $token = $user->tokens()->create([ 17 | 'name' => 'Test Token', 18 | 'token' => Str::random(40), 19 | 'abilities' => ['create', 'read'], 20 | ]); 21 | 22 | Livewire::test(ApiTokenManager::class) 23 | ->set(['apiTokenIdBeingDeleted' => $token->id]) 24 | ->call('deleteApiToken'); 25 | 26 | expect($user->fresh()->tokens)->toHaveCount(0); 27 | })->skip(function () { 28 | return ! Features::hasApiFeatures(); 29 | }, 'API support is not enabled.'); 30 | -------------------------------------------------------------------------------- /app/Providers/JetstreamServiceProvider.php: -------------------------------------------------------------------------------- 1 | configurePermissions(); 25 | 26 | Jetstream::deleteUsersUsing(DeleteUser::class); 27 | } 28 | 29 | /** 30 | * Configure the permissions that are available within the application. 31 | */ 32 | protected function configurePermissions(): void 33 | { 34 | Jetstream::defaultApiTokenPermissions(['read']); 35 | 36 | Jetstream::permissions([ 37 | 'read', 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['v1/*', 'v2/*', 'v3/*', '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 | -------------------------------------------------------------------------------- /resources/views/components/switchable-team.blade.php: -------------------------------------------------------------------------------- 1 | @props(['team', 'component' => 'dropdown-link']) 2 | 3 |
4 | @method('PUT') 5 | @csrf 6 | 7 | 8 | 9 | 10 | 11 |
12 | @if (Auth::user()->isCurrentTeam($team)) 13 | 14 | 15 | 16 | @endif 17 | 18 |
{{ $team->name }}
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /app/Models/Rate.php: -------------------------------------------------------------------------------- 1 | 'float', 42 | 'hst' => 'float', 43 | 'gst' => 'float', 44 | 'applicable' => 'float', 45 | 'start' => 'datetime' 46 | ]; 47 | } 48 | -------------------------------------------------------------------------------- /app/Providers/HorizonServiceProvider.php: -------------------------------------------------------------------------------- 1 | is_admin; 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /resources/views/emails/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/2025_02_17_203210_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->morphs('tokenable'); 17 | $table->string('name'); 18 | $table->string('token', 64)->unique(); 19 | $table->text('abilities')->nullable(); 20 | $table->timestamp('last_used_at')->nullable(); 21 | $table->timestamp('expires_at')->nullable(); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('personal_access_tokens'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /resources/views/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/components/responsive-nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = ($active ?? false) 5 | ? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out' 6 | : 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out'; 7 | @endphp 8 | 9 | merge(['class' => $classes]) }}> 10 | {{ $slot }} 11 | 12 | -------------------------------------------------------------------------------- /app/Actions/Fortify/UpdateUserPassword.php: -------------------------------------------------------------------------------- 1 | $input 18 | */ 19 | public function update(User $user, array $input): void 20 | { 21 | Validator::make($input, [ 22 | 'current_password' => ['required', 'string', 'current_password:web'], 23 | 'password' => $this->passwordRules(), 24 | ], [ 25 | 'current_password.current_password' => __('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 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | is_admin; 29 | }); 30 | 31 | RateLimiter::for('api', function (Request $request) { 32 | return $request->user() 33 | ? Limit::perMinute(config('api.rates.authed'))->by($request->user()->id) 34 | : Limit::perMinute(config('api.rates.guest'))->by($request->ip()); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Feature/CreateApiTokenTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 11 | } else { 12 | $this->actingAs($user = User::factory()->create()); 13 | } 14 | 15 | Livewire::test(ApiTokenManager::class) 16 | ->set(['createApiTokenForm' => [ 17 | 'name' => 'Test Token', 18 | 'permissions' => [ 19 | 'read', 20 | 'update', 21 | ], 22 | ]]) 23 | ->call('createApiToken'); 24 | 25 | expect($user->fresh()->tokens)->toHaveCount(1); 26 | expect($user->fresh()->tokens->first()) 27 | ->name->toEqual('Test Token') 28 | ->can('read')->toBeTrue() 29 | ->can('delete')->toBeFalse(); 30 | })->skip(function () { 31 | return ! Features::hasApiFeatures(); 32 | }, 'API support is not enabled.'); 33 | -------------------------------------------------------------------------------- /resources/views/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 9 |
10 | 11 | 12 | 13 |
14 | @csrf 15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 | {{ __('Confirm') }} 24 | 25 |
26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /tests/Feature/DeleteAccountTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 10 | 11 | Livewire::test(DeleteUserForm::class) 12 | ->set('password', 'password') 13 | ->call('deleteUser'); 14 | 15 | expect($user->fresh())->toBeNull(); 16 | })->skip(function () { 17 | return ! Features::hasAccountDeletionFeatures(); 18 | }, 'Account deletion is not enabled.'); 19 | 20 | test('correct password must be provided before account can be deleted', function () { 21 | $this->actingAs($user = User::factory()->create()); 22 | 23 | Livewire::test(DeleteUserForm::class) 24 | ->set('password', 'wrong-password') 25 | ->call('deleteUser') 26 | ->assertHasErrors(['password']); 27 | 28 | expect($user->fresh())->not->toBeNull(); 29 | })->skip(function () { 30 | return ! Features::hasAccountDeletionFeatures(); 31 | }, 'Account deletion is not enabled.'); 32 | -------------------------------------------------------------------------------- /tests/Feature/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | withPersonalTeam()->create() 9 | : User::factory()->create(); 10 | 11 | $response = $this->actingAs($user)->get('/user/confirm-password'); 12 | 13 | $response->assertStatus(200); 14 | }); 15 | 16 | test('password can be confirmed', function () { 17 | $user = User::factory()->create(); 18 | 19 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 20 | 'password' => 'password', 21 | ]); 22 | 23 | $response->assertRedirect(); 24 | $response->assertSessionHasNoErrors(); 25 | }); 26 | 27 | test('password is not confirmed with invalid password', function () { 28 | $user = User::factory()->create(); 29 | 30 | $response = $this->actingAs($user)->post('/user/confirm-password', [ 31 | 'password' => 'wrong-password', 32 | ]); 33 | 34 | $response->assertSessionHasErrors(); 35 | }); 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Canada's Sales Taxes API 2 | 3 | Here's an API to get Canadian sales tax information. Simple as that. 4 | 5 | The API is now at `v3`. 6 | 7 | ## Deprecation 8 | 9 | With the availability of `v3`, the following will be deprecated: 10 | 11 | Version 1 of the API will be deprecated on July 1st, 2025. 12 | 13 | Version 2 of the API will be deprecated on December 1st, 2025. 14 | 15 | ## Documentation 16 | 17 | - [v3 of the API](http://salestaxapi.ca/) 18 | - [v2 of the API](http://salestaxapi.ca/index-v2.html) 19 | - [v1 of the API](http://salestaxapi.ca/index-v1.html) 20 | 21 | ### Rate Limit 22 | 23 | API usage is currently rate limited to 60 hits per minute. The rate limit is subject to change upon API popularity. 24 | 25 | ### Contributing 26 | 27 | If you see any discrepancies in actual data, missing future rates, or want to contribute historical rates, please open an issue on the GitHub repository with the data and we'll include it in the API. 28 | 29 | Otherwise, you know the drill: report bugs in issues, suggest features in issues, and if you can, submit pull requests! 30 | 31 | ## License 32 | 33 | [MIT license](http://opensource.org/licenses/MIT). 34 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'token' => env('POSTMARK_TOKEN'), 19 | ], 20 | 21 | 'ses' => [ 22 | 'key' => env('AWS_ACCESS_KEY_ID'), 23 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 24 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 25 | ], 26 | 27 | 'resend' => [ 28 | 'key' => env('RESEND_KEY'), 29 | ], 30 | 31 | 'slack' => [ 32 | 'notifications' => [ 33 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 34 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 35 | ], 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /config/api.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'authed' => env('API_RATE_PER_MINUTES_AUTHED', 'Laravel'), 18 | 'guest' => env('API_RATE_PER_MINUTES_GUEST', 'Laravel'), 19 | ], 20 | 'retention' => [ 21 | 'hits' => env('HITS_RETENTION_MONTHS', 6), 22 | ], 23 | 'deprecation' => [ 24 | 'v1' => [ 25 | 'date' => "2025-01-01", 26 | 'rate' => env('DEPRECATION_RATE_V1'), 27 | ], 28 | 'v2' => [ 29 | 'date' => "2025-01-01", 30 | 'rate' => env('DEPRECATION_RATE_V2'), 31 | ], 32 | ], 33 | 'sunset' => [ 34 | 'v1' => [ 35 | 'date' => "2025-07-01", 36 | ], 37 | 'v2' => [ 38 | 'date' => "2025-12-01", 39 | ], 40 | ], 41 | 42 | ]; 43 | -------------------------------------------------------------------------------- /database/migrations/2025_02_17_203204_add_two_factor_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | text('two_factor_secret') 16 | ->after('password') 17 | ->nullable(); 18 | 19 | $table->text('two_factor_recovery_codes') 20 | ->after('two_factor_secret') 21 | ->nullable(); 22 | 23 | $table->timestamp('two_factor_confirmed_at') 24 | ->after('two_factor_recovery_codes') 25 | ->nullable(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::table('users', function (Blueprint $table) { 35 | $table->dropColumn([ 36 | 'two_factor_secret', 37 | 'two_factor_recovery_codes', 38 | 'two_factor_confirmed_at', 39 | ]); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tests/Feature/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 8 | 9 | $response->assertStatus(200); 10 | })->skip(function () { 11 | return ! Features::enabled(Features::registration()); 12 | }, 'Registration support is not enabled.'); 13 | 14 | test('registration screen cannot be rendered if support is disabled', function () { 15 | $response = $this->get('/register'); 16 | 17 | $response->assertStatus(404); 18 | })->skip(function () { 19 | return Features::enabled(Features::registration()); 20 | }, 'Registration support is enabled.'); 21 | 22 | test('new users can register', function () { 23 | $response = $this->post('/register', [ 24 | 'name' => 'Test User', 25 | 'email' => 'test@example.com', 26 | 'password' => 'password', 27 | 'password_confirmation' => 'password', 28 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(), 29 | ]); 30 | 31 | $this->assertAuthenticated(); 32 | $response->assertRedirect(route('dashboard', absolute: false)); 33 | })->skip(function () { 34 | return ! Features::enabled(Features::registration()); 35 | }, 'Registration support is not enabled.'); 36 | -------------------------------------------------------------------------------- /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 | 33 | 34 | -------------------------------------------------------------------------------- /tests/Feature/ApiTokenPermissionsTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->withPersonalTeam()->create()); 12 | } else { 13 | $this->actingAs($user = User::factory()->create()); 14 | } 15 | 16 | $token = $user->tokens()->create([ 17 | 'name' => 'Test Token', 18 | 'token' => Str::random(40), 19 | 'abilities' => ['create', 'read'], 20 | ]); 21 | 22 | Livewire::test(ApiTokenManager::class) 23 | ->set(['managingPermissionsFor' => $token]) 24 | ->set(['updateApiTokenForm' => [ 25 | 'permissions' => [ 26 | 'delete', 27 | 'missing-permission', 28 | ], 29 | ]]) 30 | ->call('updateApiToken'); 31 | 32 | expect($user->fresh()->tokens->first()) 33 | ->can('delete')->toBeTrue() 34 | ->can('read')->toBeFalse() 35 | ->can('missing-permission')->toBeFalse(); 36 | })->skip(function () { 37 | return ! Features::hasApiFeatures(); 38 | }, 'API support is not enabled.'); 39 | -------------------------------------------------------------------------------- /app/Http/Resources/v2/RateResource.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public function toArray(Request $request): array 18 | { 19 | return [ 20 | "province" => $this->when($this->province, $this->province), 21 | "start" => $this->when($this->start, $this->start->toDatetimeString()), 22 | "type" => $this->when($this->type, $this->type), 23 | "gst" => $this->when($this->gst, $this->gst), 24 | "pst" => $this->when($this->pst, $this->pst), 25 | "hst" => $this->when($this->hst, $this->hst), 26 | "applicable" => $this->when($this->applicable, $this->applicable), 27 | "source" => $this->when($this->source, $this->source), 28 | "updated_at" => $this->when($this->updated_at, $this->updated_at->toDatetimeString()), 29 | "incoming_changes" => $this->incoming_changes ? $this->incoming_changes->toDatetimeString() : false, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Traits/RequestProcessor.php: -------------------------------------------------------------------------------- 1 | extractor($request)['version']; 12 | } 13 | 14 | public function getEndpoint(Request $request) 15 | { 16 | return $this->extractor($request)['endpoint']; 17 | } 18 | 19 | public function getIP(Request $request) 20 | { 21 | return $request->ip(); 22 | } 23 | 24 | public function getUserAgent(Request $request) 25 | { 26 | return $request->hasHeader('user_agent') ? $request->header('user_agent') : null; 27 | } 28 | 29 | private function extractor(Request $request) 30 | { 31 | $segments = explode('/', $request->path()); 32 | $version = null; 33 | 34 | foreach ($segments as $key => $segment) { 35 | if (strpos($segment, 'v') === 0) { 36 | $version = $segment; 37 | unset($segments[$key]); 38 | break; 39 | } 40 | } 41 | 42 | $endpoint = implode('/', $segments); // Reconstruct the URL without the version segment 43 | 44 | return [ 45 | 'version' => $version, 46 | 'endpoint' => $endpoint 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Http/Middleware/RegisterHit.php: -------------------------------------------------------------------------------- 1 | getAPIVersion($request); 23 | $endpoint = $this->getEndpoint($request); 24 | $ip = $this->getIP($request); 25 | $user_agent = $this->getUserAgent($request); 26 | 27 | try { 28 | $response = $next($request); 29 | } catch (\Throwable $e) { 30 | app(\Illuminate\Contracts\Debug\ExceptionHandler::class)->report($e); 31 | $response = app(\Illuminate\Contracts\Debug\ExceptionHandler::class)->render($request, $e); 32 | } 33 | 34 | $statusCode = method_exists($response, 'getStatusCode') ? $response->getStatusCode() : null; 35 | 36 | ProcessHits::dispatch($version, $endpoint, $ip, $user_agent, $request->user(), $statusCode); 37 | 38 | return $response; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Actions/Fortify/CreateNewUser.php: -------------------------------------------------------------------------------- 1 | $input 19 | */ 20 | public function create(array $input): User 21 | { 22 | Validator::make($input, [ 23 | 'name' => ['required', 'string', 'max:255'], 24 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 25 | 'location' => ['required', 'string', 'max:255'], 26 | 'info' => ['nullable', 'string'], 27 | 'password' => $this->passwordRules(), 28 | 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', 29 | ])->validate(); 30 | 31 | return User::create([ 32 | 'name' => $input['name'], 33 | 'email' => $input['email'], 34 | 'location' => $input['location'], 35 | 'info' => $input['info'], 36 | 'password' => Hash::make($input['password']), 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/views/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} 9 |
10 | 11 | @session('status') 12 |
13 | {{ $value }} 14 |
15 | @endsession 16 | 17 | 18 | 19 |
20 | @csrf 21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | {{ __('Email Password Reset Link') }} 30 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Canada sales tax API " 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | API_RATE_PER_MINUTES_AUTHED=60 13 | API_RATE_PER_MINUTES_GUEST=15 14 | 15 | DEPRECATION_RATE_V1=10 16 | DEPRECATION_RATE_V2=5 17 | 18 | APP_MAINTENANCE_DRIVER=file 19 | # APP_MAINTENANCE_STORE=database 20 | 21 | PHP_CLI_SERVER_WORKERS=4 22 | 23 | BCRYPT_ROUNDS=12 24 | 25 | LOG_CHANNEL=stack 26 | LOG_STACK=single 27 | LOG_DEPRECATIONS_CHANNEL=null 28 | LOG_LEVEL=debug 29 | 30 | DB_CONNECTION=sqlite 31 | # DB_HOST=127.0.0.1 32 | # DB_PORT=3306 33 | # DB_DATABASE=laravel 34 | # DB_USERNAME=root 35 | # DB_PASSWORD= 36 | 37 | SESSION_DRIVER=database 38 | SESSION_LIFETIME=120 39 | SESSION_ENCRYPT=false 40 | SESSION_PATH=/ 41 | SESSION_DOMAIN=null 42 | 43 | BROADCAST_CONNECTION=log 44 | FILESYSTEM_DISK=local 45 | QUEUE_CONNECTION=redis 46 | 47 | CACHE_STORE=database 48 | CACHE_PREFIX= 49 | 50 | MEMCACHED_HOST=127.0.0.1 51 | 52 | REDIS_CLIENT=predis 53 | REDIS_HOST=127.0.0.1 54 | REDIS_PASSWORD=null 55 | REDIS_PORT=6379 56 | 57 | MAIL_MAILER=resend 58 | RESEND_KEY= 59 | MAIL_FROM_ADDRESS="" 60 | MAIL_FROM_NAME="${APP_NAME}" 61 | 62 | AWS_ACCESS_KEY_ID= 63 | AWS_SECRET_ACCESS_KEY= 64 | AWS_DEFAULT_REGION=us-east-1 65 | AWS_BUCKET= 66 | AWS_USE_PATH_STYLE_ENDPOINT=false 67 | 68 | VITE_APP_NAME="${APP_NAME}" 69 | -------------------------------------------------------------------------------- /database/migrations/2025_02_17_142945_create_rates_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->char('province', 2); // province abbreviation 17 | $table->decimal('pst', 6, 5); // pst, if applicable. 5 total digits, 2 after decimal 18 | $table->decimal('gst', 6, 5); // gst rate. 5 total digits, 2 after decimal 19 | $table->decimal('hst', 6, 5); // hst, if applicable, 5 total digits, 2 after decimal 20 | $table->decimal('applicable', 6, 5); // applicable tax rate. 5 total digits, 2 after decimal 21 | $table->string('type'); // type of applicable tax rate, comma separated list of types used for the applicable rate 22 | $table->datetime('start'); // when did that date start 23 | $table->text('source'); // What is the source of the information 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | */ 31 | public function down(): void 32 | { 33 | Schema::dropIfExists('rates'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /app/Http/Resources/v3/RateResource.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function toArray(Request $request): array 20 | { 21 | return [ 22 | "province" => $this->when($this->province, $this->province), 23 | "start" => $this->when($this->start, $this->start->toDatetimeString()), 24 | "type" => $this->when($this->type, $this->type), 25 | "gst" => $this->when($this->gst, $this->gst), 26 | "pst" => $this->when($this->pst, $this->pst), 27 | "hst" => $this->when($this->hst, $this->hst), 28 | "applicable" => $this->when($this->applicable, $this->applicable), 29 | "source" => $this->when($this->source, $this->source), 30 | "updated_at" => $this->when($this->updated_at, $this->updated_at->toDatetimeString()), 31 | "incoming_changes" => $this->incoming_changes ? $this->incoming_changes->toDatetimeString() : false, 32 | ]; 33 | } 34 | 35 | public function with(Request $request): array 36 | { 37 | return $this->generateMeta(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /resources/views/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 | -------------------------------------------------------------------------------- /app/Http/Controllers/HitController.php: -------------------------------------------------------------------------------- 1 | Hit::orderBy('created_at', 'desc')->paginate(10), 20 | ]); 21 | } 22 | 23 | /** 24 | * Show the form for creating a new resource. 25 | */ 26 | public function create() 27 | { 28 | // 29 | } 30 | 31 | /** 32 | * Store a newly created resource in storage. 33 | */ 34 | public function store(Request $request) 35 | { 36 | // 37 | } 38 | 39 | /** 40 | * Display the specified resource. 41 | */ 42 | public function show(Hit $hit) 43 | { 44 | // 45 | } 46 | 47 | /** 48 | * Show the form for editing the specified resource. 49 | */ 50 | public function edit(Hit $hit) 51 | { 52 | // 53 | } 54 | 55 | /** 56 | * Update the specified resource in storage. 57 | */ 58 | public function update(Request $request, Hit $hit) 59 | { 60 | // 61 | } 62 | 63 | /** 64 | * Remove the specified resource from storage. 65 | */ 66 | public function destroy(Hit $hit) 67 | { 68 | // 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) 16 | 17 | 18 | @livewireStyles 19 | 20 | 21 | 22 | 23 |
24 | @livewire('navigation-menu') 25 | 26 | 27 | @if (isset($header)) 28 |
29 |
30 | {{ $header }} 31 |
32 |
33 | @endif 34 | 35 | 36 |
37 | {{ $slot }} 38 |
39 |
40 | 41 | @stack('modals') 42 | 43 | @livewireScripts 44 | 45 | 46 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white dark:bg-gray-700', 'dropdownClasses' => '']) 2 | 3 | @php 4 | $alignmentClasses = match ($align) { 5 | 'left' => 'ltr:origin-top-left rtl:origin-top-right start-0', 6 | 'top' => 'origin-top', 7 | 'none', 'false' => '', 8 | default => 'ltr:origin-top-right rtl:origin-top-left end-0', 9 | }; 10 | 11 | $width = match ($width) { 12 | '48' => 'w-48', 13 | '60' => 'w-60', 14 | default => 'w-48', 15 | }; 16 | @endphp 17 | 18 |
19 |
20 | {{ $trigger }} 21 |
22 | 23 | 37 |
38 | -------------------------------------------------------------------------------- /app/Http/Controllers/UsageController.php: -------------------------------------------------------------------------------- 1 | Usage::orderBy('updated_at', 'desc')->paginate(10), 21 | ]); 22 | } 23 | 24 | /** 25 | * Show the form for creating a new resource. 26 | */ 27 | public function create() 28 | { 29 | // 30 | } 31 | 32 | /** 33 | * Store a newly created resource in storage. 34 | */ 35 | public function store(Request $request) 36 | { 37 | // 38 | } 39 | 40 | /** 41 | * Display the specified resource. 42 | */ 43 | public function show(Rate $rate) 44 | { 45 | // 46 | } 47 | 48 | /** 49 | * Show the form for editing the specified resource. 50 | */ 51 | public function edit(Rate $rate) 52 | { 53 | // 54 | } 55 | 56 | /** 57 | * Update the specified resource in storage. 58 | */ 59 | public function update(Request $request, Rate $rate) 60 | { 61 | // 62 | } 63 | 64 | /** 65 | * Remove the specified resource from storage. 66 | */ 67 | public function destroy(Rate $rate) 68 | { 69 | // 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Providers/FortifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | input(Fortify::username())).'|'.$request->ip()); 38 | 39 | return Limit::perMinute(5)->by($throttleKey); 40 | }); 41 | 42 | RateLimiter::for('two-factor', function (Request $request) { 43 | return Limit::perMinute(5)->by($request->session()->get('login.id')); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /resources/views/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | @csrf 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 | 24 |
25 | 26 | 27 |
28 | 29 |
30 | 31 | {{ __('Reset Password') }} 32 | 33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /resources/views/profile/show.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Profile') }} 5 |

6 |
7 | 8 |
9 |
10 | @if (Laravel\Fortify\Features::canUpdateProfileInformation()) 11 | @livewire('profile.update-profile-information-form') 12 | 13 | 14 | @endif 15 | 16 | @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords())) 17 |
18 | @livewire('profile.update-password-form') 19 |
20 | 21 | 22 | @endif 23 | 24 | @if (Laravel\Fortify\Features::canManageTwoFactorAuthentication()) 25 |
26 | @livewire('profile.two-factor-authentication-form') 27 |
28 | 29 | 30 | @endif 31 | 32 |
33 | @livewire('profile.logout-other-browser-sessions-form') 34 |
35 | 36 | @if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures()) 37 | 38 | 39 |
40 | @livewire('profile.delete-user-form') 41 |
42 | @endif 43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /app/Policies/AlertPolicy.php: -------------------------------------------------------------------------------- 1 | is_admin 17 | ? Response::allow() 18 | : Response::deny('You are not authorized to do this.'); 19 | } 20 | 21 | /** 22 | * Determine whether the user can view the model. 23 | */ 24 | public function view(User $user, Alert $alert): Response 25 | { 26 | return $user->is_admin 27 | ? Response::allow() 28 | : Response::deny('You are not authorized to do this.'); 29 | } 30 | 31 | /** 32 | * Determine whether the user can create models. 33 | */ 34 | public function create(User $user): Response 35 | { 36 | return $user->is_admin 37 | ? Response::allow() 38 | : Response::deny('You are not authorized to do this.'); 39 | } 40 | 41 | /** 42 | * Determine whether the user can update the model. 43 | */ 44 | public function update(User $user, Alert $alert): Response 45 | { 46 | return $user->is_admin 47 | ? Response::allow() 48 | : Response::deny('You are not authorized to do this.'); 49 | } 50 | 51 | /** 52 | * Determine whether the user can delete the model. 53 | */ 54 | public function delete(User $user, Alert $alert): Response 55 | { 56 | return $user->is_admin 57 | ? Response::allow() 58 | : Response::deny('You are not authorized to do this.'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Policies/HitPolicy.php: -------------------------------------------------------------------------------- 1 | is_admin 17 | ? Response::allow() 18 | : Response::deny('You are not authorized to view hits.'); 19 | } 20 | 21 | /** 22 | * Determine whether the user can view the model. 23 | */ 24 | public function view(User $user, Hit $hit): Response 25 | { 26 | return $user->is_admin 27 | ? Response::allow() 28 | : Response::deny('You are not authorized to view this hit.'); 29 | } 30 | 31 | /** 32 | * Determine whether the user can create models. 33 | */ 34 | public function create(User $user): Response 35 | { 36 | return $user->is_admin 37 | ? Response::allow() 38 | : Response::deny('You are not authorized to add hits.'); 39 | } 40 | 41 | /** 42 | * Determine whether the user can update the model. 43 | */ 44 | public function update(User $user, Hit $hit): Response 45 | { 46 | return $user->is_admin 47 | ? Response::allow() 48 | : Response::deny('You are not authorized to edit this hit.'); 49 | } 50 | 51 | /** 52 | * Determine whether the user can delete the model. 53 | */ 54 | public function delete(User $user, Hit $hit): Response 55 | { 56 | return $user->is_admin 57 | ? Response::allow() 58 | : Response::deny('You are not authorized to delete this hit.'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | extend(Tests\TestCase::class) 15 | ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) 16 | ->in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | function something() 45 | { 46 | // .. 47 | } 48 | -------------------------------------------------------------------------------- /app/Policies/RatePolicy.php: -------------------------------------------------------------------------------- 1 | is_admin 17 | ? Response::allow() 18 | : Response::deny('You are not authorized to view rates.'); 19 | } 20 | 21 | /** 22 | * Determine whether the user can view the model. 23 | */ 24 | public function view(User $user, Rate $rate): Response 25 | { 26 | return $user->is_admin 27 | ? Response::allow() 28 | : Response::deny('You are not authorized to view this rate.'); 29 | } 30 | 31 | /** 32 | * Determine whether the user can create models. 33 | */ 34 | public function create(User $user): Response 35 | { 36 | return $user->is_admin 37 | ? Response::allow() 38 | : Response::deny('You are not authorized to add rates.'); 39 | } 40 | 41 | /** 42 | * Determine whether the user can update the model. 43 | */ 44 | public function update(User $user, Rate $rate): Response 45 | { 46 | return $user->is_admin 47 | ? Response::allow() 48 | : Response::deny('You are not authorized to edit this rate.'); 49 | } 50 | 51 | /** 52 | * Determine whether the user can delete the model. 53 | */ 54 | public function delete(User $user, Rate $rate): Response 55 | { 56 | return $user->is_admin 57 | ? Response::allow() 58 | : Response::deny('You are not authorized to delete this rate.'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/Policies/UsagePolicy.php: -------------------------------------------------------------------------------- 1 | is_admin 17 | ? Response::allow() 18 | : Response::deny('You are not authorized to view usages.'); 19 | } 20 | 21 | /** 22 | * Determine whether the user can view the model. 23 | */ 24 | public function view(User $user, Usage $usage): Response 25 | { 26 | return $user->is_admin 27 | ? Response::allow() 28 | : Response::deny('You are not authorized to view this usage.'); 29 | } 30 | 31 | /** 32 | * Determine whether the user can create models. 33 | */ 34 | public function create(User $user): Response 35 | { 36 | return $user->is_admin 37 | ? Response::allow() 38 | : Response::deny('You are not authorized to add usages.'); 39 | } 40 | 41 | /** 42 | * Determine whether the user can update the model. 43 | */ 44 | public function update(User $user, Usage $usage): Response 45 | { 46 | return $user->is_admin 47 | ? Response::allow() 48 | : Response::deny('You are not authorized to edit this usage.'); 49 | } 50 | 51 | /** 52 | * Determine whether the user can delete the model. 53 | */ 54 | public function delete(User $user, Usage $usage): Response 55 | { 56 | return $user->is_admin 57 | ? Response::allow() 58 | : Response::deny('You are not authorized to delete this usage.'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /resources/views/profile/update-password-form.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ __('Update Password') }} 4 | 5 | 6 | 7 | {{ __('Ensure your account is using a long, random password to stay secure.') }} 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 | {{ __('Saved.') }} 33 | 34 | 35 | 36 | {{ __('Save') }} 37 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /tests/Feature/UpdatePasswordTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()); 10 | 11 | Livewire::test(UpdatePasswordForm::class) 12 | ->set('state', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]) 17 | ->call('updatePassword'); 18 | 19 | expect(Hash::check('new-password', $user->fresh()->password))->toBeTrue(); 20 | }); 21 | 22 | test('current password must be correct', function () { 23 | $this->actingAs($user = User::factory()->create()); 24 | 25 | Livewire::test(UpdatePasswordForm::class) 26 | ->set('state', [ 27 | 'current_password' => 'wrong-password', 28 | 'password' => 'new-password', 29 | 'password_confirmation' => 'new-password', 30 | ]) 31 | ->call('updatePassword') 32 | ->assertHasErrors(['current_password']); 33 | 34 | expect(Hash::check('password', $user->fresh()->password))->toBeTrue(); 35 | }); 36 | 37 | test('new passwords must match', function () { 38 | $this->actingAs($user = User::factory()->create()); 39 | 40 | Livewire::test(UpdatePasswordForm::class) 41 | ->set('state', [ 42 | 'current_password' => 'password', 43 | 'password' => 'new-password', 44 | 'password_confirmation' => 'wrong-password', 45 | ]) 46 | ->call('updatePassword') 47 | ->assertHasErrors(['password']); 48 | 49 | expect(Hash::check('password', $user->fresh()->password))->toBeTrue(); 50 | }); 51 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->foreignId('current_team_id')->nullable(); 22 | $table->string('profile_photo_path', 2048)->nullable(); 23 | $table->timestamps(); 24 | }); 25 | 26 | Schema::create('password_reset_tokens', function (Blueprint $table) { 27 | $table->string('email')->primary(); 28 | $table->string('token'); 29 | $table->timestamp('created_at')->nullable(); 30 | }); 31 | 32 | Schema::create('sessions', function (Blueprint $table) { 33 | $table->string('id')->primary(); 34 | $table->foreignId('user_id')->nullable()->index(); 35 | $table->string('ip_address', 45)->nullable(); 36 | $table->text('user_agent')->nullable(); 37 | $table->longText('payload'); 38 | $table->integer('last_activity')->index(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | */ 45 | public function down(): void 46 | { 47 | Schema::dropIfExists('users'); 48 | Schema::dropIfExists('password_reset_tokens'); 49 | Schema::dropIfExists('sessions'); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /resources/views/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/Actions/Fortify/UpdateUserProfileInformation.php: -------------------------------------------------------------------------------- 1 | $input 17 | */ 18 | public function update(User $user, array $input): void 19 | { 20 | Validator::make($input, [ 21 | 'name' => ['required', 'string', 'max:255'], 22 | 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], 23 | 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], 24 | ])->validateWithBag('updateProfileInformation'); 25 | 26 | if (isset($input['photo'])) { 27 | $user->updateProfilePhoto($input['photo']); 28 | } 29 | 30 | if ($input['email'] !== $user->email && 31 | $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 array $input 45 | */ 46 | protected function updateVerifiedUser(User $user, array $input): void 47 | { 48 | $user->forceFill([ 49 | 'name' => $input['name'], 50 | 'email' => $input['email'], 51 | 'email_verified_at' => null, 52 | ])->save(); 53 | 54 | $user->sendEmailVerificationNotification(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Jobs/ProcessHits.php: -------------------------------------------------------------------------------- 1 | version = strtolower($version); 28 | $this->endpoint = strtolower($endpoint); 29 | $this->ip = $ip; 30 | $this->user_agent = $user_agent; 31 | $this->user = $user; 32 | $this->status_code = $status_code; 33 | } 34 | 35 | /** 36 | * Execute the job. 37 | */ 38 | public function handle(): void 39 | { 40 | $usage = Usage::firstOrCreate(['version' => $this->version, 'endpoint' => $this->endpoint]); 41 | $usage->increment('count'); 42 | 43 | if ($this->user) { 44 | $this->user->hits()->create([ 45 | 'version' => $this->version, 46 | 'endpoint' => $this->endpoint, 47 | 'client' => $this->ip, 48 | 'user_agent' => $this->user_agent, 49 | 'status_code' => $this->status_code, 50 | ]); 51 | } else { 52 | Hit::create([ 53 | 'version' => $this->version, 54 | 'endpoint' => $this->endpoint, 55 | 'client' => $this->ip, 56 | 'user_agent' => $this->user_agent, 57 | 'status_code' => $this->status_code, 58 | ]); 59 | } 60 | // dd($this->version, $this->endpoint, $this->ip, $this->user_agent, $this->user); 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/views/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/Http/Middleware/DeprecationMiddleware.php: -------------------------------------------------------------------------------- 1 | toRfc7231String() . '. Please read the documentation at https://salestaxapi.ca/ for more information.'; 24 | 25 | if ($rate === 100) { 26 | return response() 27 | ->json(['message' => $message], 402) 28 | ->withHeaders([ 29 | 'X-Deprecation-Notice' => $message, 30 | 'Deprecation' => $deprecationDate->toRfc7231String(), 31 | 'Sunset' => $sunsetDate->toRfc7231String(), 32 | ]); 33 | } 34 | 35 | $response = $next($request); 36 | 37 | $response->headers->set('X-Deprecation-Notice', $message); 38 | $response->headers->set('Deprecation', $deprecationDate->toRfc7231String()); 39 | $response->headers->set('Sunset', $sunsetDate->toRfc7231String()); 40 | 41 | if ($rate > 0 && mt_rand(1, 100) <= $rate) { 42 | $response->setStatusCode(402); // sending subset of requests a non standard code to catch their attention 43 | } 44 | 45 | return $response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /tests/Feature/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | withPersonalTeam()->create([ 11 | 'email_verified_at' => null, 12 | ]); 13 | 14 | $response = $this->actingAs($user)->get('/email/verify'); 15 | 16 | $response->assertStatus(200); 17 | })->skip(function () { 18 | return ! Features::enabled(Features::emailVerification()); 19 | }, 'Email verification not enabled.'); 20 | 21 | test('email can be verified', function () { 22 | Event::fake(); 23 | 24 | $user = User::factory()->create([ 25 | 'email_verified_at' => null, 26 | ]); 27 | 28 | $verificationUrl = URL::temporarySignedRoute( 29 | 'verification.verify', 30 | now()->addMinutes(60), 31 | ['id' => $user->id, 'hash' => sha1($user->email)] 32 | ); 33 | 34 | $response = $this->actingAs($user)->get($verificationUrl); 35 | 36 | Event::assertDispatched(Verified::class); 37 | 38 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 39 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 40 | })->skip(function () { 41 | return ! Features::enabled(Features::emailVerification()); 42 | }, 'Email verification not enabled.'); 43 | 44 | test('email can not verified with invalid hash', function () { 45 | $user = User::factory()->create([ 46 | 'email_verified_at' => null, 47 | ]); 48 | 49 | $verificationUrl = URL::temporarySignedRoute( 50 | 'verification.verify', 51 | now()->addMinutes(60), 52 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 53 | ); 54 | 55 | $this->actingAs($user)->get($verificationUrl); 56 | 57 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 58 | })->skip(function () { 59 | return ! Features::enabled(Features::emailVerification()); 60 | }, 'Email verification not enabled.'); 61 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | */ 18 | use HasFactory; 19 | use HasProfilePhoto; 20 | use Notifiable; 21 | use TwoFactorAuthenticatable; 22 | 23 | /** 24 | * The attributes that are mass assignable. 25 | * 26 | * @var array 27 | */ 28 | protected $fillable = [ 29 | 'name', 30 | 'email', 31 | 'password', 32 | 'location', 33 | 'info', 34 | ]; 35 | 36 | /** 37 | * The attributes that should be hidden for serialization. 38 | * 39 | * @var array 40 | */ 41 | protected $hidden = [ 42 | 'password', 43 | 'remember_token', 44 | 'two_factor_recovery_codes', 45 | 'two_factor_secret', 46 | 'current_team_id', 47 | 'profile_photo_path', 48 | 'is_admin', 49 | 'two_factor_confirmed_at', 50 | 'profile_photo_url', 51 | 'id' 52 | ]; 53 | 54 | /** 55 | * The accessors to append to the model's array form. 56 | * 57 | * @var array 58 | */ 59 | protected $appends = [ 60 | 'profile_photo_url', 61 | ]; 62 | 63 | /** 64 | * Get the attributes that should be cast. 65 | * 66 | * @return array 67 | */ 68 | protected function casts(): array 69 | { 70 | return [ 71 | 'email_verified_at' => 'datetime', 72 | 'password' => 'hashed', 73 | 'is_admin' => 'boolean', 74 | ]; 75 | } 76 | 77 | public function hits() 78 | { 79 | return $this->hasMany(Hit::class); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /resources/views/auth/login.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{-- 4 | 5 | --}} 6 | 7 | 8 | 9 | @session('status') 10 |
11 | {{ $value }} 12 |
13 | @endsession 14 | 15 |
16 | @csrf 17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 | 33 |
34 | 35 |
36 | @if (Route::has('password.request')) 37 | 38 | {{ __('Forgot your password?') }} 39 | 40 | @endif 41 | 42 | 43 | {{ __('Log in') }} 44 | 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /resources/views/auth/verify-email.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | {{ __('Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} 9 |
10 | 11 | @if (session('status') == 'verification-link-sent') 12 |
13 | {{ __('A new verification link has been sent to the email address you provided in your profile settings.') }} 14 |
15 | @endif 16 | 17 |
18 |
19 | @csrf 20 | 21 |
22 | 23 | {{ __('Resend Verification Email') }} 24 | 25 |
26 |
27 | 28 |
29 | 33 | {{ __('Edit Profile') }} 34 | 35 |
36 | @csrf 37 | 38 | 41 |
42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /tests/Feature/TwoFactorAuthenticationSettingsTest.php: -------------------------------------------------------------------------------- 1 | actingAs($user = User::factory()->create()->fresh()); 10 | 11 | $this->withSession(['auth.password_confirmed_at' => time()]); 12 | 13 | Livewire::test(TwoFactorAuthenticationForm::class) 14 | ->call('enableTwoFactorAuthentication'); 15 | 16 | $user = $user->fresh(); 17 | 18 | expect($user->two_factor_secret)->not->toBeNull(); 19 | expect($user->recoveryCodes())->toHaveCount(8); 20 | })->skip(function () { 21 | return ! Features::canManageTwoFactorAuthentication(); 22 | }, 'Two factor authentication is not enabled.'); 23 | 24 | test('recovery codes can be regenerated', function () { 25 | $this->actingAs($user = User::factory()->create()->fresh()); 26 | 27 | $this->withSession(['auth.password_confirmed_at' => time()]); 28 | 29 | $component = Livewire::test(TwoFactorAuthenticationForm::class) 30 | ->call('enableTwoFactorAuthentication') 31 | ->call('regenerateRecoveryCodes'); 32 | 33 | $user = $user->fresh(); 34 | 35 | $component->call('regenerateRecoveryCodes'); 36 | 37 | expect($user->recoveryCodes())->toHaveCount(8); 38 | expect(array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes()))->toHaveCount(8); 39 | })->skip(function () { 40 | return ! Features::canManageTwoFactorAuthentication(); 41 | }, 'Two factor authentication is not enabled.'); 42 | 43 | test('two factor authentication can be disabled', function () { 44 | $this->actingAs($user = User::factory()->create()->fresh()); 45 | 46 | $this->withSession(['auth.password_confirmed_at' => time()]); 47 | 48 | $component = Livewire::test(TwoFactorAuthenticationForm::class) 49 | ->call('enableTwoFactorAuthentication'); 50 | 51 | $this->assertNotNull($user->fresh()->two_factor_secret); 52 | 53 | $component->call('disableTwoFactorAuthentication'); 54 | 55 | expect($user->fresh()->two_factor_secret)->toBeNull(); 56 | })->skip(function () { 57 | return ! Features::canManageTwoFactorAuthentication(); 58 | }, 'Two factor authentication is not enabled.'); 59 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class UserFactory extends Factory 16 | { 17 | /** 18 | * The current password being used by the factory. 19 | */ 20 | protected static ?string $password; 21 | 22 | /** 23 | * Define the model's default state. 24 | * 25 | * @return array 26 | */ 27 | public function definition(): array 28 | { 29 | return [ 30 | 'name' => fake()->name(), 31 | 'email' => fake()->unique()->safeEmail(), 32 | 'email_verified_at' => now(), 33 | 'location' => fake()->city(), 34 | 'info' => fake()->sentence(), 35 | 'password' => static::$password ??= Hash::make('password'), 36 | 'two_factor_secret' => null, 37 | 'two_factor_recovery_codes' => null, 38 | 'remember_token' => Str::random(10), 39 | 'profile_photo_path' => null, 40 | 'current_team_id' => null, 41 | ]; 42 | } 43 | 44 | /** 45 | * Indicate that the model's email address should be unverified. 46 | */ 47 | public function unverified(): static 48 | { 49 | return $this->state(fn(array $attributes) => [ 50 | 'email_verified_at' => null, 51 | ]); 52 | } 53 | 54 | /** 55 | * Indicate that the user should have a personal team. 56 | */ 57 | public function withPersonalTeam(?callable $callback = null): static 58 | { 59 | if (! Features::hasTeamFeatures()) { 60 | return $this->state([]); 61 | } 62 | 63 | return $this->has( 64 | Team::factory() 65 | ->state(fn(array $attributes, User $user) => [ 66 | 'name' => $user->name . '\'s Team', 67 | 'user_id' => $user->id, 68 | 'personal_team' => true, 69 | ]) 70 | ->when(is_callable($callback), $callback), 71 | 'ownedTeams' 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Feature/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 10 | 11 | $response->assertStatus(200); 12 | })->skip(function () { 13 | return ! Features::enabled(Features::resetPasswords()); 14 | }, 'Password updates are not enabled.'); 15 | 16 | test('reset password link can be requested', function () { 17 | Notification::fake(); 18 | 19 | $user = User::factory()->create(); 20 | 21 | $response = $this->post('/forgot-password', [ 22 | 'email' => $user->email, 23 | ]); 24 | 25 | Notification::assertSentTo($user, ResetPassword::class); 26 | })->skip(function () { 27 | return ! Features::enabled(Features::resetPasswords()); 28 | }, 'Password updates are not enabled.'); 29 | 30 | test('reset password screen can be rendered', function () { 31 | Notification::fake(); 32 | 33 | $user = User::factory()->create(); 34 | 35 | $response = $this->post('/forgot-password', [ 36 | 'email' => $user->email, 37 | ]); 38 | 39 | Notification::assertSentTo($user, ResetPassword::class, function (object $notification) { 40 | $response = $this->get('/reset-password/'.$notification->token); 41 | 42 | $response->assertStatus(200); 43 | 44 | return true; 45 | }); 46 | })->skip(function () { 47 | return ! Features::enabled(Features::resetPasswords()); 48 | }, 'Password updates are not enabled.'); 49 | 50 | test('password can be reset with valid token', function () { 51 | Notification::fake(); 52 | 53 | $user = User::factory()->create(); 54 | 55 | $response = $this->post('/forgot-password', [ 56 | 'email' => $user->email, 57 | ]); 58 | 59 | Notification::assertSentTo($user, ResetPassword::class, function (object $notification) use ($user) { 60 | $response = $this->post('/reset-password', [ 61 | 'token' => $notification->token, 62 | 'email' => $user->email, 63 | 'password' => 'password', 64 | 'password_confirmation' => 'password', 65 | ]); 66 | 67 | $response->assertSessionHasNoErrors(); 68 | 69 | return true; 70 | }); 71 | })->skip(function () { 72 | return ! Features::enabled(Features::resetPasswords()); 73 | }, 'Password updates are not enabled.'); 74 | -------------------------------------------------------------------------------- /resources/views/profile/delete-user-form.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ __('Delete Account') }} 4 | 5 | 6 | 7 | {{ __('Permanently delete your account.') }} 8 | 9 | 10 | 11 |
12 | {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} 13 |
14 | 15 |
16 | 17 | {{ __('Delete Account') }} 18 | 19 |
20 | 21 | 22 | 23 | 24 | {{ __('Delete Account') }} 25 | 26 | 27 | 28 | {{ __('Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} 29 | 30 |
31 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | {{ __('Cancel') }} 45 | 46 | 47 | 48 | {{ __('Delete Account') }} 49 | 50 | 51 |
52 |
53 |
54 | -------------------------------------------------------------------------------- /resources/markdown/terms.md: -------------------------------------------------------------------------------- 1 | # Terms and Conditions for API Usage 2 | 3 | **Last Updated:** February 25, 2025 4 | 5 | ## 1. Acceptance of Terms 6 | 7 | By registering for an account and using our API ("Service"), you agree to be bound by these Terms and Conditions. If you do not agree, you must not use the Service. 8 | 9 | ## 2. Account Registration and Security 10 | 11 | Users must register an account to access the API. You are responsible for maintaining the confidentiality of your account credentials and for all activities conducted under your account. We are not liable for any unauthorized use of your account. 12 | 13 | ## 3. License and Usage Restrictions 14 | 15 | We grant you a limited, non-exclusive, non-transferable, and revocable license to use the API solely for its intended purpose. You agree not to: 16 | 17 | - Use the API for unlawful or malicious purposes. 18 | - Reverse engineer, modify, or create derivative works based on the API. 19 | - Exceed usage limits or abuse the Service in any manner. 20 | 21 | ## 4. Data and Privacy 22 | 23 | Your use of the API is subject to our Privacy Policy, which governs how we collect, use, and protect personal information. You are responsible for ensuring compliance with all applicable data protection laws. 24 | 25 | ## 5. Disclaimers and Limitation of Liability 26 | 27 | - The API is provided "as is" and "as available" without warranties of any kind, express or implied. 28 | - We do not guarantee that the API will be uninterrupted, error-free, or secure. 29 | - To the fullest extent permitted by law, we disclaim all liability for any damages, losses, or claims arising from your use of or inability to use the API, including but not limited to data loss, business interruption, or financial loss. 30 | 31 | ## 6. Indemnification 32 | 33 | You agree to indemnify and hold us harmless from any claims, damages, or liabilities resulting from your use of the API, your violation of these terms, or any applicable laws. 34 | 35 | ## 7. Modifications and Termination 36 | 37 | We reserve the right to modify or discontinue the API at any time without prior notice. We may also update these Terms and Conditions, and continued use of the API constitutes acceptance of the revised terms. 38 | 39 | ## 8. Governing Law and Jurisdiction 40 | 41 | These Terms and Conditions are governed by the laws of Québec, Canada. Any disputes arising from these terms shall be resolved in the courts of Québec. 42 | 43 | ## 9. Contact Information 44 | 45 | For questions regarding these Terms and Conditions, please contact us at [jp@jp@salestaxapi.ca](mailto:jp@salestaxapi.ca). 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/API/v1/RateController.php: -------------------------------------------------------------------------------- 1 | orderBy('start', 'DESC')->first(); 15 | 16 | return [ 17 | 'rate' => $rate->gst, 18 | 'last_modified' => $rate->updated_at->toDateString(), 19 | ]; 20 | }); 21 | } 22 | 23 | private function provinceRates($province, $field) 24 | { 25 | if ($province == 'all') { 26 | $rates = Rate::where('province', '!=', 'FE')->get()->groupBy('province'); 27 | 28 | $rates = $rates->map(function ($rates, $province) use ($field) { 29 | $rates = $rates->sortByDesc('start'); 30 | $rate = $rates->first(); 31 | 32 | return [ 33 | 'rate' => ($rate->$field), 34 | 'last_modified' => $rate->updated_at->toDateString(), 35 | ]; 36 | })->toArray(); 37 | 38 | ksort($rates); 39 | 40 | return response()->json($rates); 41 | } else { 42 | $rate = Rate::where('province', $province)->orderBy('start', 'DESC')->first(); 43 | 44 | if ($rate->$field == 0) { 45 | return response()->json(['code' => 1000, 'message' => "There is no applicable {$field} in the {$province} region"]); 46 | } 47 | 48 | return [ 49 | 'rate' => ($rate->$field), 50 | 'last_modified' => $rate->updated_at->toDateString(), 51 | ]; 52 | } 53 | } 54 | 55 | public function getHst($province) 56 | { 57 | return Cache::remember("v1-hst-{$province}", 86400, function () use ($province) { 58 | return $this->provinceRates($province, 'hst'); 59 | }); 60 | } 61 | 62 | public function getPst($province) 63 | { 64 | return Cache::remember("pst-{$province}", 86400, function () use ($province) { 65 | return $this->provinceRates($province, 'pst'); 66 | }); 67 | } 68 | 69 | public function getTotal($province) 70 | { 71 | return Cache::remember("applicable-{$province}", 86400, function () use ($province) { 72 | return $this->provinceRates($province, 'applicable'); 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resources/views/alerts/create.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Add an alert') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 |
13 | @csrf 14 | @method('post') 15 | 16 |
17 | 18 | 19 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | {{ __('Add') }} 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Below you may configure as many filesystem disks as necessary, and you 24 | | may even configure multiple disks for the same driver. Examples for 25 | | most supported storage drivers are configured here for reference. 26 | | 27 | | Supported drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app/private'), 36 | 'serve' => true, 37 | 'throw' => false, 38 | 'report' => false, 39 | ], 40 | 41 | 'public' => [ 42 | 'driver' => 'local', 43 | 'root' => storage_path('app/public'), 44 | 'url' => env('APP_URL').'/storage', 45 | 'visibility' => 'public', 46 | 'throw' => false, 47 | 'report' => false, 48 | ], 49 | 50 | 's3' => [ 51 | 'driver' => 's3', 52 | 'key' => env('AWS_ACCESS_KEY_ID'), 53 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 54 | 'region' => env('AWS_DEFAULT_REGION'), 55 | 'bucket' => env('AWS_BUCKET'), 56 | 'url' => env('AWS_URL'), 57 | 'endpoint' => env('AWS_ENDPOINT'), 58 | 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 59 | 'throw' => false, 60 | 'report' => false, 61 | ], 62 | 63 | ], 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Symbolic Links 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Here you may configure the symbolic links that will be created when the 71 | | `storage:link` Artisan command is executed. The array keys should be 72 | | the locations of the links and the values should be their targets. 73 | | 74 | */ 75 | 76 | 'links' => [ 77 | public_path('storage') => storage_path('app/public'), 78 | ], 79 | 80 | ]; 81 | -------------------------------------------------------------------------------- /resources/views/alerts/edit.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Edit an alert') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 |
13 | @csrf 14 | @method('put') 15 | 16 |
17 | 18 | 19 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 | active ? 'checked' : '' }}> 40 | 41 | 42 |
43 | 44 |
45 | {{ __('Update') }} 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://getcomposer.org/schema.json", 3 | "name": "laravel/laravel", 4 | "type": "project", 5 | "description": "The skeleton application for the Laravel framework.", 6 | "keywords": [ 7 | "laravel", 8 | "framework" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "^8.2", 13 | "laravel/framework": "^12", 14 | "laravel/horizon": "^5.30", 15 | "laravel/jetstream": "^5.3", 16 | "laravel/pulse": "^1.4", 17 | "laravel/sanctum": "^4.0", 18 | "laravel/tinker": "^2.9", 19 | "livewire/livewire": "^3.0", 20 | "predis/predis": "^2.0", 21 | "resend/resend-php": "^0.15.1" 22 | }, 23 | "require-dev": { 24 | "fakerphp/faker": "^1.23", 25 | "laravel/pail": "^1.1", 26 | "laravel/pint": "^1.13", 27 | "laravel/sail": "^1.26", 28 | "mockery/mockery": "^1.6", 29 | "nunomaduro/collision": "^8.1", 30 | "pestphp/pest": "^3.7", 31 | "pestphp/pest-plugin-laravel": "^3.1" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "App\\": "app/", 36 | "Database\\Factories\\": "database/factories/", 37 | "Database\\Seeders\\": "database/seeders/" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Tests\\": "tests/" 43 | } 44 | }, 45 | "scripts": { 46 | "post-autoload-dump": [ 47 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 48 | "@php artisan package:discover --ansi" 49 | ], 50 | "post-update-cmd": [ 51 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 52 | ], 53 | "post-root-package-install": [ 54 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 55 | ], 56 | "post-create-project-cmd": [ 57 | "@php artisan key:generate --ansi", 58 | "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", 59 | "@php artisan migrate --graceful --ansi" 60 | ], 61 | "dev": [ 62 | "Composer\\Config::disableProcessTimeout", 63 | "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite" 64 | ] 65 | }, 66 | "extra": { 67 | "laravel": { 68 | "dont-discover": [] 69 | } 70 | }, 71 | "config": { 72 | "optimize-autoloader": true, 73 | "preferred-install": "dist", 74 | "sort-packages": true, 75 | "allow-plugins": { 76 | "pestphp/pest-plugin": true, 77 | "php-http/discovery": true 78 | } 79 | }, 80 | "minimum-stability": "stable", 81 | "prefer-stable": true 82 | } 83 | -------------------------------------------------------------------------------- /resources/markdown/policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 3 | **Last Updated:** February 25, 2025 4 | 5 | ## 1. Introduction 6 | 7 | This Privacy Policy explains how we collect, use, and protect personal information related to the use of our API ("Service"). By using the Service, you agree to the practices described in this policy. 8 | 9 | ## 2. Information We Collect 10 | 11 | We may collect the following types of information: 12 | 13 | - **Account Information**: When you register for an account, we collect details such as your name, email address, and other necessary credentials. 14 | - **Usage Data**: We may collect logs and metadata related to your use of the API, including request details, timestamps, and associated identifiers. 15 | - **Technical Data**: Information about your device, browser, and network may be collected to enhance security and improve performance. 16 | 17 | ## 3. How We Use Your Information 18 | 19 | We use the collected data for the following purposes: 20 | 21 | - To provide and maintain the API service. 22 | - To improve and optimize API performance. 23 | - To monitor and prevent security threats, fraud, or abuse. 24 | - To communicate with users regarding service updates or support requests. 25 | - To comply with legal obligations. 26 | 27 | ## 4. Data Sharing and Disclosure 28 | 29 | We do not sell or rent personal information to third parties. However, we may disclose data under the following circumstances: 30 | 31 | - To comply with legal requirements, such as court orders or regulatory requests. 32 | - To enforce our Terms and Conditions. 33 | - To protect the rights, safety, and security of our users and services. 34 | - In the event of a business transfer, such as a merger or acquisition. 35 | 36 | ## 5. Data Security 37 | 38 | We implement appropriate technical and organizational measures to protect personal data from unauthorized access, loss, or disclosure. However, no system is completely secure, and we cannot guarantee absolute security. 39 | 40 | ## 6. Data Retention 41 | 42 | We retain personal data for as long as necessary to fulfill the purposes outlined in this policy, unless a longer retention period is required by law. 43 | 44 | ## 7. User Rights 45 | 46 | Users in Québec, Canada, have rights under applicable privacy laws, including: 47 | 48 | - The right to access and correct personal information. 49 | - The right to request data deletion, subject to legal obligations. 50 | - The right to withdraw consent where applicable. 51 | Requests regarding user rights can be submitted to [jp@salestaxapi.ca](mailto:jp@salestaxapi.ca). 52 | 53 | ## 8. Changes to This Policy 54 | 55 | We may update this Privacy Policy from time to time. Continued use of the API after changes are made constitutes acceptance of the revised policy. 56 | 57 | ## 9. Contact Information 58 | 59 | If you have any questions about this Privacy Policy, please contact us at [jp@salestaxapi.ca](mailto:jp@salestaxapi.ca). 60 | -------------------------------------------------------------------------------- /resources/views/components/welcome.blade.php: -------------------------------------------------------------------------------- 1 |
2 |

3 | Canada's sale taxes API 4 |

5 | 6 |

7 | Thank you for using the Sales Tax API. To get started, please make sure to obtain your API key from the corresponding page. Remember, caching the results on your end can significantly reduce the load on our servers and improve your application's performance. 8 |

9 | 10 |

11 | This project is free and maintained by volunteers. If you find this service useful, please consider making a donation to support its development and maintenance. Your contributions help keep the API running smoothly and allow us to continue improving it. 12 |

13 | 14 |

15 | 16 | Sponsor this project on Github 17 | 18 | 19 | 20 | 21 | 22 |

23 | 24 |

25 | 26 | Star the project on Github 27 | 28 | 29 | 30 | 31 | 32 |

33 | 34 |

35 | 36 | Read the documentation 37 | 38 | 39 | 40 | 41 | 42 |

43 |
44 | -------------------------------------------------------------------------------- /app/Http/Controllers/AlertController.php: -------------------------------------------------------------------------------- 1 | with([ 19 | 'alerts' => Alert::orderBy('created_at', 'DESC')->paginate('10'), 20 | ]); 21 | } 22 | 23 | /** 24 | * Show the form for creating a new resource. 25 | */ 26 | public function create() 27 | { 28 | Gate::authorize('create', Alert::class); 29 | 30 | return view('alerts.create'); 31 | } 32 | 33 | /** 34 | * Store a newly created resource in storage. 35 | */ 36 | public function store(Request $request) 37 | { 38 | Gate::authorize('create', Alert::class); 39 | 40 | $validated = $request->validate([ 41 | 'type' => 'required|string|max:255', 42 | 'message' => 'required|string|max:255', 43 | 'active' => 'nullable|boolean', 44 | ]); 45 | 46 | $alert = new Alert(); 47 | $alert->type = $validated['type']; 48 | $alert->message = $validated['message']; 49 | $alert->active = isset($validated['active']) ? true : false; 50 | $alert->save(); 51 | 52 | return redirect()->route('alerts.index')->with('success', 'Alert created successfully.'); 53 | } 54 | 55 | /** 56 | * Display the specified resource. 57 | */ 58 | public function show(Alert $alert) 59 | { 60 | // 61 | } 62 | 63 | /** 64 | * Show the form for editing the specified resource. 65 | */ 66 | public function edit(Alert $alert) 67 | { 68 | return view('alerts.edit')->with([ 69 | 'alert' => $alert, 70 | ]); 71 | } 72 | 73 | /** 74 | * Update the specified resource in storage. 75 | */ 76 | public function update(Request $request, Alert $alert) 77 | { 78 | Gate::authorize('update', $alert); 79 | 80 | $validated = $request->validate([ 81 | 'type' => 'required|string|max:255', 82 | 'message' => 'required|string|max:255', 83 | 'active' => 'nullable', 84 | ]); 85 | 86 | $alert->type = $validated['type']; 87 | $alert->message = $validated['message']; 88 | $alert->active = isset($validated['active']) ? true : false; 89 | $alert->save(); 90 | 91 | return redirect()->route('alerts.index')->with('success', 'Alert updated successfully.'); 92 | } 93 | 94 | /** 95 | * Remove the specified resource from storage. 96 | */ 97 | public function destroy(Alert $alert) 98 | { 99 | // 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /config/jetstream.php: -------------------------------------------------------------------------------- 1 | 'livewire', 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Jetstream Route Middleware 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may specify which middleware Jetstream will assign to the routes 27 | | that it registers with the application. When necessary, you may modify 28 | | these middleware; however, this default value is usually sufficient. 29 | | 30 | */ 31 | 32 | 'middleware' => ['web'], 33 | 34 | 'auth_session' => AuthenticateSession::class, 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Jetstream Guard 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Here you may specify the authentication guard Jetstream will use while 42 | | authenticating users. This value should correspond with one of your 43 | | guards that is already present in your "auth" configuration file. 44 | | 45 | */ 46 | 47 | 'guard' => 'sanctum', 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Features 52 | |-------------------------------------------------------------------------- 53 | | 54 | | Some of Jetstream's features are optional. You may disable the features 55 | | by removing them from this array. You're free to only remove some of 56 | | these features or you can even remove all of these if you need to. 57 | | 58 | */ 59 | 60 | 'features' => [ 61 | Features::termsAndPrivacyPolicy(), 62 | // Features::profilePhotos(), 63 | Features::api(), 64 | // Features::teams(['invitations' => true]), 65 | Features::accountDeletion(), 66 | ], 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Profile Photo Disk 71 | |-------------------------------------------------------------------------- 72 | | 73 | | This configuration value determines the default disk that will be used 74 | | when storing profile photos for your application's users. Typically 75 | | this will be the "public" disk but you may adjust this if needed. 76 | | 77 | */ 78 | 79 | 'profile_photo_disk' => 'public', 80 | 81 | ]; 82 | -------------------------------------------------------------------------------- /resources/views/auth/two-factor-challenge.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | {{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }} 10 |
11 | 12 |
13 | {{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }} 14 |
15 | 16 | 17 | 18 |
19 | @csrf 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 40 | 41 | 50 | 51 | 52 | {{ __('Log in') }} 53 | 54 |
55 |
56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /app/Http/Controllers/RateController.php: -------------------------------------------------------------------------------- 1 | Rate::orderBy('start', 'desc')->paginate(10), 20 | ]); 21 | } 22 | 23 | /** 24 | * Show the form for creating a new resource. 25 | */ 26 | public function create() 27 | { 28 | Gate::authorize('create', Rate::class); 29 | 30 | return view('rates.create'); 31 | } 32 | 33 | /** 34 | * Store a newly created resource in storage. 35 | */ 36 | public function store(Request $request) 37 | { 38 | Gate::authorize('create', Rate::class); 39 | 40 | $validated = $request->validate([ 41 | 'province' => 'required|string|max:2', 42 | 'start' => 'required|date', 43 | 'pst' => 'required|numeric|min:0', 44 | 'gst' => 'required|numeric|min:0', 45 | 'hst' => 'required|numeric|min:0', 46 | 'applicable' => 'required|numeric|min:0', 47 | 'type' => 'required|string', 48 | 'source' => 'required|string', 49 | ]); 50 | 51 | Rate::create($validated); 52 | 53 | return redirect()->route('dashboard')->with('success', 'Rate added successfully.'); 54 | } 55 | 56 | /** 57 | * Display the specified resource. 58 | */ 59 | public function show(Rate $rate) 60 | { 61 | // 62 | } 63 | 64 | /** 65 | * Show the form for editing the specified resource. 66 | */ 67 | public function edit(Rate $rate) 68 | { 69 | Gate::authorize('update', $rate); 70 | 71 | return view('rates.edit', compact('rate')); 72 | } 73 | 74 | /** 75 | * Update the specified resource in storage. 76 | */ 77 | public function update(Request $request, Rate $rate) 78 | { 79 | Gate::authorize('update', $rate); 80 | 81 | $validated = $request->validate([ 82 | 'province' => 'required|string|max:2', 83 | 'start' => 'required|date', 84 | 'pst' => 'required|numeric|min:0', 85 | 'gst' => 'required|numeric|min:0', 86 | 'hst' => 'required|numeric|min:0', 87 | 'applicable' => 'required|numeric|min:0', 88 | 'type' => 'required|string', 89 | 'source' => 'required|string', 90 | ]); 91 | 92 | $rate->update($validated); 93 | 94 | return redirect()->route('dashboard')->with('success', 'Rate updated successfully.'); 95 | } 96 | 97 | /** 98 | * Remove the specified resource from storage. 99 | */ 100 | public function destroy(Rate $rate) 101 | { 102 | // 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | middleware(['auth:sanctum', 'throttle:api']) 12 | ->group(function () { 13 | Route::get('/token-test', [UserController::class, 'show']); 14 | 15 | Route::group(['prefix' => '/federal'], function () { 16 | Route::get('/gst/historical', [V3RateController::class, 'getHistoricalGst']); 17 | Route::get('/gst/future', [V3RateController::class, 'getFutureGst']); 18 | Route::get('/gst', [V3RateController::class, 'getCurrentGst']); 19 | }); 20 | 21 | Route::group(['prefix' => '/province'], function () { 22 | Route::get('/all', [V3RateController::class, 'getAllPst']); 23 | Route::get('/{province}/historical', [V3RateController::class, 'getHistoricalPst']); 24 | Route::get('/{province}/future', [V3RateController::class, 'getFuturePst']); 25 | Route::get('/{province}', [V3RateController::class, 'getCurrentPst']); 26 | }); 27 | }); 28 | 29 | Route::prefix('/v2')->middleware(['throttle:api'])->middleware(DeprecationMiddleware::class . ':v2') 30 | ->group(function () { 31 | Route::group(['prefix' => '/federal'], function () { 32 | Route::get('/gst/historical', [V2RateController::class, 'getHistoricalGst']); 33 | Route::get('/gst/future', [V2RateController::class, 'getFutureGst']); 34 | Route::get('/gst', [V2RateController::class, 'getCurrentGst']); 35 | }); 36 | 37 | Route::group(['prefix' => '/province'], function () { 38 | Route::get('/all', [V2RateController::class, 'getAllPst']); 39 | Route::get('/{province}/historical', [V2RateController::class, 'getHistoricalPst']); 40 | Route::get('/{province}/future', [V2RateController::class, 'getFuturePst']); 41 | Route::get('/{province}', [V2RateController::class, 'getCurrentPst']); 42 | }); 43 | }); 44 | 45 | Route::prefix('/v1')->middleware(['throttle:api'])->middleware(DeprecationMiddleware::class . ':v1') 46 | ->group(function () { 47 | Route::prefix('/federal')->group(function () { 48 | Route::get('/gst', [V1RateController::class, 'getGst']); 49 | Route::get('/hst/{prov}', [V1RateController::class, 'getHst']); 50 | Route::get('/hst/all', [V1RateController::class, 'getHst']); 51 | }); 52 | 53 | Route::group(['prefix' => '/provincial'], function () { 54 | Route::get('/pst/{prov}', [V1RateController::class, 'getPst']); 55 | Route::get('/pst/all', [V1RateController::class, 'getPst']); 56 | }); 57 | 58 | Route::get('/total/{province}', [V1RateController::class, 'getTotal']); 59 | Route::get('/total/all', [V1RateController::class, 'getTotal']); 60 | }); 61 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 19 | '%s%s', 20 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 21 | Sanctum::currentApplicationUrlWithPort() 22 | ))), 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Sanctum Guards 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This array contains the authentication guards that will be checked when 30 | | Sanctum is trying to authenticate a request. If none of these guards 31 | | are able to authenticate the request, Sanctum will use the bearer 32 | | token that's present on an incoming request for authentication. 33 | | 34 | */ 35 | 36 | 'guard' => ['web'], 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Expiration Minutes 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This value controls the number of minutes until an issued token will be 44 | | considered expired. This will override any values set in the token's 45 | | "expires_at" attribute, but first-party sessions are not affected. 46 | | 47 | */ 48 | 49 | 'expiration' => null, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Token Prefix 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Sanctum can prefix new tokens in order to take advantage of numerous 57 | | security scanning initiatives maintained by open source platforms 58 | | that notify developers if they commit tokens into repositories. 59 | | 60 | | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning 61 | | 62 | */ 63 | 64 | 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Sanctum Middleware 69 | |-------------------------------------------------------------------------- 70 | | 71 | | When authenticating your first-party SPA with Sanctum you may need to 72 | | customize some of the middleware Sanctum uses while processing the 73 | | request. You may change the middleware listed below as required. 74 | | 75 | */ 76 | 77 | 'middleware' => [ 78 | 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 79 | 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, 80 | 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, 81 | ], 82 | 83 | ]; 84 | -------------------------------------------------------------------------------- /resources/views/components/application-logo.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /database/migrations/2025_02_19_163416_create_pulse_tables.php: -------------------------------------------------------------------------------- 1 | shouldRun()) { 15 | return; 16 | } 17 | 18 | Schema::create('pulse_values', function (Blueprint $table) { 19 | $table->id(); 20 | $table->unsignedInteger('timestamp'); 21 | $table->string('type'); 22 | $table->mediumText('key'); 23 | match ($this->driver()) { 24 | 'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 25 | 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), 26 | 'sqlite' => $table->string('key_hash'), 27 | }; 28 | $table->mediumText('value'); 29 | 30 | $table->index('timestamp'); // For trimming... 31 | $table->index('type'); // For fast lookups and purging... 32 | $table->unique(['type', 'key_hash']); // For data integrity and upserts... 33 | }); 34 | 35 | Schema::create('pulse_entries', function (Blueprint $table) { 36 | $table->id(); 37 | $table->unsignedInteger('timestamp'); 38 | $table->string('type'); 39 | $table->mediumText('key'); 40 | match ($this->driver()) { 41 | 'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 42 | 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), 43 | 'sqlite' => $table->string('key_hash'), 44 | }; 45 | $table->bigInteger('value')->nullable(); 46 | 47 | $table->index('timestamp'); // For trimming... 48 | $table->index('type'); // For purging... 49 | $table->index('key_hash'); // For mapping... 50 | $table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries... 51 | }); 52 | 53 | Schema::create('pulse_aggregates', function (Blueprint $table) { 54 | $table->id(); 55 | $table->unsignedInteger('bucket'); 56 | $table->unsignedMediumInteger('period'); 57 | $table->string('type'); 58 | $table->mediumText('key'); 59 | match ($this->driver()) { 60 | 'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'), 61 | 'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'), 62 | 'sqlite' => $table->string('key_hash'), 63 | }; 64 | $table->string('aggregate'); 65 | $table->decimal('value', 20, 2); 66 | $table->unsignedInteger('count')->nullable(); 67 | 68 | $table->unique(['bucket', 'period', 'type', 'aggregate', 'key_hash']); // Force "on duplicate update"... 69 | $table->index(['period', 'bucket']); // For trimming... 70 | $table->index('type'); // For purging... 71 | $table->index(['period', 'type', 'aggregate', 'bucket']); // For aggregate queries... 72 | }); 73 | } 74 | 75 | /** 76 | * Reverse the migrations. 77 | */ 78 | public function down(): void 79 | { 80 | Schema::dropIfExists('pulse_values'); 81 | Schema::dropIfExists('pulse_entries'); 82 | Schema::dropIfExists('pulse_aggregates'); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /resources/views/components/banner.blade.php: -------------------------------------------------------------------------------- 1 | @props(['style' => session('flash.bannerStyle', 'success'), 'message' => session('flash.banner')]) 2 | 3 |
12 |
13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |

32 |
33 | 34 |
35 | 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /resources/views/usages/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Usages') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 19 | 22 | 25 | 28 | 29 | 30 | 31 | @forelse ($usages as $usage) 32 | 33 | 36 | 39 | 42 | 45 | 46 | @empty 47 | 48 | 51 | 52 | @endforelse 53 | 54 |
17 | Version 18 | 20 | Endpoint 21 | 23 | Count 24 | 26 | Latest hit 27 |
34 | {{ $usage->version }} 35 | 37 | {{ $usage->endpoint }} 38 | 40 | {{ $usage->count }} 41 | 43 | {{ $usage->updated_at->diffForHumans() }} 44 |
49 | No usage yet. 50 |
55 |
56 |
57 | {{ $usages->links() }} 58 |
59 |
60 |
61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_STORE', 'database'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "array", "database", "file", "memcached", 30 | | "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'array' => [ 37 | 'driver' => 'array', 38 | 'serialize' => false, 39 | ], 40 | 41 | 'database' => [ 42 | 'driver' => 'database', 43 | 'connection' => env('DB_CACHE_CONNECTION'), 44 | 'table' => env('DB_CACHE_TABLE', 'cache'), 45 | 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 46 | 'lock_table' => env('DB_CACHE_LOCK_TABLE'), 47 | ], 48 | 49 | 'file' => [ 50 | 'driver' => 'file', 51 | 'path' => storage_path('framework/cache/data'), 52 | 'lock_path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 77 | 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | 'octane' => [ 90 | 'driver' => 'octane', 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Cache Key Prefix 98 | |-------------------------------------------------------------------------- 99 | | 100 | | When utilizing the APC, database, memcached, Redis, and DynamoDB cache 101 | | stores, there might be other applications using the same cache. For 102 | | that reason, you may prefix every cache key to avoid collisions. 103 | | 104 | */ 105 | 106 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), 107 | 108 | ]; 109 | -------------------------------------------------------------------------------- /resources/views/alerts/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | {{ __('Alerts') }} 5 |

6 |
7 | 8 |
9 |
10 |
11 |
12 |
13 | Create a new alert 14 | 15 | 16 | 17 | 20 | 23 | 26 | 29 | 30 | 31 | 32 | @forelse ($alerts as $alert) 33 | 34 | 37 | 40 | 43 | 46 | 47 | @empty 48 | 49 | 52 | 53 | @endforelse 54 | 55 |
18 | Type 19 | 21 | Message 22 | 24 | Active 25 | 27 | Actions 28 |
35 | {{ $alert->type }} 36 | 38 | {{ $alert->message }} 39 | 41 | {{ $alert->active ? "Yes" : "No" }} 42 | 44 | Edit 45 |
50 | No alerts yet. 51 |
56 |
57 |
58 | {{ $alerts->links() }} 59 |
60 |
61 |
62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'log'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Mailer Configurations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Here you may configure all of the mailers used by your application plus 25 | | their respective settings. Several examples have been configured for 26 | | you and you are free to add your own as your application requires. 27 | | 28 | | Laravel supports a variety of mail "transport" drivers that can be used 29 | | when delivering an email. You may specify which one you're using for 30 | | your mailers below. You may also add additional mailers if needed. 31 | | 32 | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", 33 | | "postmark", "resend", "log", "array", 34 | | "failover", "roundrobin" 35 | | 36 | */ 37 | 38 | 'mailers' => [ 39 | 40 | 'smtp' => [ 41 | 'transport' => 'smtp', 42 | 'scheme' => env('MAIL_SCHEME'), 43 | 'url' => env('MAIL_URL'), 44 | 'host' => env('MAIL_HOST', '127.0.0.1'), 45 | 'port' => env('MAIL_PORT', 2525), 46 | 'username' => env('MAIL_USERNAME'), 47 | 'password' => env('MAIL_PASSWORD'), 48 | 'timeout' => null, 49 | 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)), 50 | ], 51 | 52 | 'ses' => [ 53 | 'transport' => 'ses', 54 | ], 55 | 56 | 'postmark' => [ 57 | 'transport' => 'postmark', 58 | // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), 59 | // 'client' => [ 60 | // 'timeout' => 5, 61 | // ], 62 | ], 63 | 64 | 'resend' => [ 65 | 'transport' => 'resend', 66 | ], 67 | 68 | 'sendmail' => [ 69 | 'transport' => 'sendmail', 70 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), 71 | ], 72 | 73 | 'log' => [ 74 | 'transport' => 'log', 75 | 'channel' => env('MAIL_LOG_CHANNEL'), 76 | ], 77 | 78 | 'array' => [ 79 | 'transport' => 'array', 80 | ], 81 | 82 | 'failover' => [ 83 | 'transport' => 'failover', 84 | 'mailers' => [ 85 | 'smtp', 86 | 'log', 87 | ], 88 | ], 89 | 90 | 'roundrobin' => [ 91 | 'transport' => 'roundrobin', 92 | 'mailers' => [ 93 | 'ses', 94 | 'postmark', 95 | ], 96 | ], 97 | 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Global "From" Address 103 | |-------------------------------------------------------------------------- 104 | | 105 | | You may wish for all emails sent by your application to be sent from 106 | | the same address. Here you may specify a name and address that is 107 | | used globally for all emails that are sent by your application. 108 | | 109 | */ 110 | 111 | 'from' => [ 112 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 113 | 'name' => env('MAIL_FROM_NAME', 'Example'), 114 | ], 115 | 116 | ]; 117 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'database'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection options for every queue backend 24 | | used by your application. An example configuration is provided for 25 | | each backend supported by Laravel. You're also free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'connection' => env('DB_QUEUE_CONNECTION'), 40 | 'table' => env('DB_QUEUE_TABLE', 'jobs'), 41 | 'queue' => env('DB_QUEUE', 'default'), 42 | 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 43 | 'after_commit' => false, 44 | ], 45 | 46 | 'beanstalkd' => [ 47 | 'driver' => 'beanstalkd', 48 | 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), 49 | 'queue' => env('BEANSTALKD_QUEUE', 'default'), 50 | 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 51 | 'block_for' => 0, 52 | 'after_commit' => false, 53 | ], 54 | 55 | 'sqs' => [ 56 | 'driver' => 'sqs', 57 | 'key' => env('AWS_ACCESS_KEY_ID'), 58 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 59 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 60 | 'queue' => env('SQS_QUEUE', 'default'), 61 | 'suffix' => env('SQS_SUFFIX'), 62 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 63 | 'after_commit' => false, 64 | ], 65 | 66 | 'redis' => [ 67 | 'driver' => 'redis', 68 | 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 69 | 'queue' => env('REDIS_QUEUE', 'default'), 70 | 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 71 | 'block_for' => null, 72 | 'after_commit' => false, 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Job Batching 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The following options configure the database and table that store job 83 | | batching information. These options can be updated to any database 84 | | connection and table which has been defined by your application. 85 | | 86 | */ 87 | 88 | 'batching' => [ 89 | 'database' => env('DB_CONNECTION', 'sqlite'), 90 | 'table' => 'job_batches', 91 | ], 92 | 93 | /* 94 | |-------------------------------------------------------------------------- 95 | | Failed Queue Jobs 96 | |-------------------------------------------------------------------------- 97 | | 98 | | These options configure the behavior of failed queue job logging so you 99 | | can control how and where failed jobs are stored. Laravel ships with 100 | | support for storing failed jobs in a simple file or in a database. 101 | | 102 | | Supported drivers: "database-uuids", "dynamodb", "file", "null" 103 | | 104 | */ 105 | 106 | 'failed' => [ 107 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 108 | 'database' => env('DB_CONNECTION', 'sqlite'), 109 | 'table' => 'failed_jobs', 110 | ], 111 | 112 | ]; 113 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => env('AUTH_GUARD', 'web'), 18 | 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | which utilizes session storage plus the Eloquent user provider. 29 | | 30 | | All authentication guards have a user provider, which defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | system used by the application. Typically, Eloquent is utilized. 33 | | 34 | | Supported: "session" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | ], 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | User Providers 48 | |-------------------------------------------------------------------------- 49 | | 50 | | All authentication guards have a user provider, which defines how the 51 | | users are actually retrieved out of your database or other storage 52 | | system used by the application. Typically, Eloquent is utilized. 53 | | 54 | | If you have multiple user tables or models you may configure multiple 55 | | providers to represent the model / table. These providers may then 56 | | be assigned to any extra authentication guards you have defined. 57 | | 58 | | Supported: "database", "eloquent" 59 | | 60 | */ 61 | 62 | 'providers' => [ 63 | 'users' => [ 64 | 'driver' => 'eloquent', 65 | 'model' => env('AUTH_MODEL', App\Models\User::class), 66 | ], 67 | 68 | // 'users' => [ 69 | // 'driver' => 'database', 70 | // 'table' => 'users', 71 | // ], 72 | ], 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Resetting Passwords 77 | |-------------------------------------------------------------------------- 78 | | 79 | | These configuration options specify the behavior of Laravel's password 80 | | reset functionality, including the table utilized for token storage 81 | | and the user provider that is invoked to actually retrieve users. 82 | | 83 | | The expiry time is the number of minutes that each reset token will be 84 | | considered valid. This security feature keeps tokens short-lived so 85 | | they have less time to be guessed. You may change this as needed. 86 | | 87 | | The throttle setting is the number of seconds a user must wait before 88 | | generating more password reset tokens. This prevents the user from 89 | | quickly generating a very large amount of password reset tokens. 90 | | 91 | */ 92 | 93 | 'passwords' => [ 94 | 'users' => [ 95 | 'provider' => 'users', 96 | 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 97 | 'expire' => 60, 98 | 'throttle' => 60, 99 | ], 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Password Confirmation Timeout 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Here you may define the amount of seconds before a password confirmation 108 | | window expires and users are asked to re-enter their password via the 109 | | confirmation screen. By default, the timeout lasts for three hours. 110 | | 111 | */ 112 | 113 | 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 114 | 115 | ]; 116 | --------------------------------------------------------------------------------