diff --git a/website/.claude/skills/tailwindcss-development/SKILL.md b/website/.claude/skills/tailwindcss-development/SKILL.md new file mode 100644 index 0000000..a778ab8 --- /dev/null +++ b/website/.claude/skills/tailwindcss-development/SKILL.md @@ -0,0 +1,124 @@ +--- +name: tailwindcss-development +description: >- + Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, + working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, + typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, + hero section, cards, buttons, or any visual/UI changes. +--- + +# Tailwind CSS Development + +## When to Apply + +Activate this skill when: + +- Adding styles to components or pages +- Working with responsive design +- Implementing dark mode +- Extracting repeated patterns into components +- Debugging spacing or layout issues + +## Documentation + +Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation. + +## Basic Usage + +- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns. +- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue). +- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically. + +## Tailwind CSS v4 Specifics + +- Always use Tailwind CSS v4 and avoid deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. + +### CSS-First Configuration + +In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed: + + +@theme { + --color-brand: oklch(0.72 0.11 178); +} + + +### Import Syntax + +In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3: + + +- @tailwind base; +- @tailwind components; +- @tailwind utilities; ++ @import "tailwindcss"; + + +### Replaced Utilities + +Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric. + +| Deprecated | Replacement | +|------------|-------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | + +## Spacing + +Use `gap` utilities instead of margins for spacing between siblings: + + +
+
Item 1
+
Item 2
+
+
+ +## Dark Mode + +If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant: + + +
+ Content adapts to color scheme +
+
+ +## Common Patterns + +### Flexbox Layout + + +
+
Left content
+
Right content
+
+
+ +### Grid Layout + + +
+
Card 1
+
Card 2
+
Card 3
+
+
+ +## Common Pitfalls + +- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.) +- Using `@tailwind` directives instead of `@import "tailwindcss"` +- Trying to use `tailwind.config.js` instead of CSS `@theme` directive +- Using margins for spacing between siblings instead of gap utilities +- Forgetting to add dark mode variants when the project uses dark mode \ No newline at end of file diff --git a/website/.env.example b/website/.env.example index d700016..c63804d 100644 --- a/website/.env.example +++ b/website/.env.example @@ -1,8 +1,8 @@ -APP_NAME=Laravel +APP_NAME="EZSCALE Billing" APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost +APP_URL=http://ezscale.dev APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -20,46 +20,70 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=mariadb +DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 -DB_DATABASE=ezscale -DB_USERNAME=root +DB_DATABASE=ezscale_billing +DB_USERNAME=ezscale DB_PASSWORD= -SESSION_DRIVER=database +SESSION_DRIVER=redis SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ -SESSION_DOMAIN=null +SESSION_DOMAIN=.ezscale.dev BROADCAST_CONNECTION=log FILESYSTEM_DISK=local -QUEUE_CONNECTION=database +QUEUE_CONNECTION=redis -CACHE_STORE=database +CACHE_STORE=redis # CACHE_PREFIX= -MEMCACHED_HOST=127.0.0.1 - REDIS_CLIENT=phpredis REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379 +# Domain Routing +DOMAIN_MARKETING=ezscale.dev +DOMAIN_ACCOUNT=account.ezscale.dev +DOMAIN_ADMIN=admin.ezscale.dev + MAIL_MAILER=log MAIL_SCHEME=null MAIL_HOST=127.0.0.1 MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null -MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_ADDRESS="noreply@ezscale.cloud" MAIL_FROM_NAME="${APP_NAME}" -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false +# Stripe +STRIPE_KEY= +STRIPE_SECRET= +STRIPE_WEBHOOK_SECRET= + +# PayPal +PAYPAL_MODE=sandbox +PAYPAL_SANDBOX_CLIENT_ID= +PAYPAL_SANDBOX_CLIENT_SECRET= + +# Discord Admin Alerts +DISCORD_WEBHOOK_URL= + +# Provisioning APIs +VIRTFUSION_API_URL= +VIRTFUSION_API_KEY= +PTERODACTYL_API_URL= +PTERODACTYL_API_KEY= +SYNERGYCP_API_URL= +SYNERGYCP_API_KEY= +ENHANCE_API_URL= +ENHANCE_API_KEY= + +# SupportPal +SUPPORTPAL_API_URL= +SUPPORTPAL_API_KEY= VITE_APP_NAME="${APP_NAME}" diff --git a/website/CLAUDE.md b/website/CLAUDE.md index cf45950..c72a0d9 100644 --- a/website/CLAUDE.md +++ b/website/CLAUDE.md @@ -9,20 +9,26 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.3.11 +- php - 8.3.6 +- inertiajs/inertia-laravel (INERTIA) - v2 +- laravel/cashier (CASHIER) - v16 +- laravel/fortify (FORTIFY) - v1 - laravel/framework (LARAVEL) - v12 +- laravel/passport (PASSPORT) - v13 - laravel/prompts (PROMPTS) - v0 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 - pestphp/pest (PEST) - v4 - phpunit/phpunit (PHPUNIT) - v12 +- tailwindcss (TAILWINDCSS) - v4 ## Skills Activation This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck. - `pest-testing` — Tests applications using the Pest 4 PHP framework. Activates when writing tests, creating unit or feature tests, adding assertions, testing Livewire components, browser testing, debugging test failures, working with datasets or mocking; or when the user mentions test, spec, TDD, expects, assertion, coverage, or needs to verify functionality works. +- `tailwindcss-development` — Styles applications using Tailwind CSS v4 utilities. Activates when adding styles, restyling components, working with gradients, spacing, layout, flex, grid, responsive design, dark mode, colors, typography, or borders; or when the user mentions CSS, styling, classes, Tailwind, restyle, hero section, cards, buttons, or any visual/UI changes. ## Conventions @@ -126,6 +132,22 @@ protected function isAccessible(User $user, ?string $path = null): bool - Add useful array shape type definitions when appropriate. +=== inertia-laravel/core rules === + +# Inertia + +- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns. +- Components live in `resources/js/Pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views. +- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples. + +=== inertia-laravel/v2 rules === + +# Inertia v2 + +- Use all Inertia features from v1 and v2. Check the documentation before making changes to ensure the correct approach. +- New features: deferred props, infinite scrolling (merging props + `WhenVisible`), lazy loading on scroll, polling, prefetching. +- When using deferred props, add an empty state with a pulsing or animated skeleton. + === laravel/core rules === # Do Things the Laravel Way @@ -222,4 +244,12 @@ protected function isAccessible(User $user, ?string $path = null): bool - Do NOT delete tests without approval. - CRITICAL: ALWAYS use `search-docs` tool for version-specific Pest documentation and updated code examples. - IMPORTANT: Activate `pest-testing` every time you're working with a Pest or testing-related task. + +=== tailwindcss/core rules === + +# Tailwind CSS + +- Always use existing Tailwind conventions; check project patterns before adding new ones. +- IMPORTANT: Always use `search-docs` tool for version-specific Tailwind CSS documentation and updated code examples. Never rely on training data. +- IMPORTANT: Activate `tailwindcss-development` every time you're working with a Tailwind CSS or styling-related task. diff --git a/website/app/Actions/Fortify/CreateNewUser.php b/website/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..3859f81 --- /dev/null +++ b/website/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,46 @@ + $input */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'password' => $this->passwordRules(), + ])->validate(); + + return DB::transaction(function () use ($input): User { + $user = User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + + $user->assignRole('customer'); + $user->profile()->create([]); + + return $user; + }); + } +} diff --git a/website/app/Actions/Fortify/PasswordValidationRules.php b/website/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 0000000..2dbaec4 --- /dev/null +++ b/website/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,18 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } +} diff --git a/website/app/Actions/Fortify/ResetUserPassword.php b/website/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..aa78219 --- /dev/null +++ b/website/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/website/app/Actions/Fortify/UpdateUserPassword.php b/website/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 0000000..15b86aa --- /dev/null +++ b/website/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,32 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => $this->passwordRules(), + ], [ + 'current_password.current_password' => __('The provided password does not match your current password.'), + ])->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/website/app/Actions/Fortify/UpdateUserProfileInformation.php b/website/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..53f9385 --- /dev/null +++ b/website/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,58 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ])->validateWithBag('updateProfileInformation'); + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/website/app/Http/Controllers/Account/DashboardController.php b/website/app/Http/Controllers/Account/DashboardController.php new file mode 100644 index 0000000..24caad2 --- /dev/null +++ b/website/app/Http/Controllers/Account/DashboardController.php @@ -0,0 +1,23 @@ +user(); + + return Inertia::render('Dashboard', [ + 'servicesCount' => $user->services()->count(), + 'activeServicesCount' => $user->services()->where('status', 'active')->count(), + ]); + } +} diff --git a/website/app/Http/Controllers/Account/ProfileController.php b/website/app/Http/Controllers/Account/ProfileController.php new file mode 100644 index 0000000..524b1a9 --- /dev/null +++ b/website/app/Http/Controllers/Account/ProfileController.php @@ -0,0 +1,33 @@ + $request->user()->load('profile'), + ]); + } + + public function update(Request $request): \Illuminate\Http\RedirectResponse + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'phone' => ['nullable', 'string', 'max:20'], + 'company' => ['nullable', 'string', 'max:255'], + ]); + + $request->user()->update($validated); + + return back(); + } +} diff --git a/website/app/Http/Controllers/Admin/DashboardController.php b/website/app/Http/Controllers/Admin/DashboardController.php new file mode 100644 index 0000000..3fbb89e --- /dev/null +++ b/website/app/Http/Controllers/Admin/DashboardController.php @@ -0,0 +1,23 @@ + User::role('customer')->count(), + 'totalServices' => Service::count(), + 'activeServices' => Service::where('status', 'active')->count(), + ]); + } +} diff --git a/website/app/Http/Middleware/EnsureUserNotSuspended.php b/website/app/Http/Middleware/EnsureUserNotSuspended.php new file mode 100644 index 0000000..c5f16e9 --- /dev/null +++ b/website/app/Http/Middleware/EnsureUserNotSuspended.php @@ -0,0 +1,21 @@ +user()?->isSuspended()) { + abort(403, 'Your account has been suspended.'); + } + + return $next($request); + } +} diff --git a/website/app/Http/Responses/LoginResponse.php b/website/app/Http/Responses/LoginResponse.php new file mode 100644 index 0000000..d9ac37c --- /dev/null +++ b/website/app/Http/Responses/LoginResponse.php @@ -0,0 +1,27 @@ +user(); + + if ($user && $user->hasRole('admin')) { + $url = 'https://'.config('app.domains.admin').'/dashboard'; + + return Inertia::location($url); + } + + return redirect()->intended('/dashboard'); + } +} diff --git a/website/app/Models/Announcement.php b/website/app/Models/Announcement.php new file mode 100644 index 0000000..c706745 --- /dev/null +++ b/website/app/Models/Announcement.php @@ -0,0 +1,39 @@ + 'datetime', + 'expires_at' => 'datetime', + ]; + } + + public function isPublished(): bool + { + return $this->published_at !== null && $this->published_at->isPast(); + } + + public function isExpired(): bool + { + return $this->expires_at !== null && $this->expires_at->isPast(); + } +} diff --git a/website/app/Models/AuditLog.php b/website/app/Models/AuditLog.php new file mode 100644 index 0000000..e37e034 --- /dev/null +++ b/website/app/Models/AuditLog.php @@ -0,0 +1,43 @@ + 'array', + 'resource_id' => 'integer', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function admin(): BelongsTo + { + return $this->belongsTo(User::class, 'admin_id'); + } +} diff --git a/website/app/Models/BandwidthUsage.php b/website/app/Models/BandwidthUsage.php new file mode 100644 index 0000000..b7c9611 --- /dev/null +++ b/website/app/Models/BandwidthUsage.php @@ -0,0 +1,48 @@ + 'datetime', + 'period_end' => 'datetime', + 'bytes_in' => 'integer', + 'bytes_out' => 'integer', + 'total_bytes' => 'integer', + 'quota_bytes' => 'integer', + 'overage_bytes' => 'integer', + 'overage_charge' => 'decimal:2', + ]; + } + + public function service(): BelongsTo + { + return $this->belongsTo(Service::class); + } +} diff --git a/website/app/Models/Coupon.php b/website/app/Models/Coupon.php new file mode 100644 index 0000000..078c0af --- /dev/null +++ b/website/app/Models/Coupon.php @@ -0,0 +1,54 @@ + 'decimal:2', + 'applies_to' => 'array', + 'max_uses' => 'integer', + 'times_used' => 'integer', + 'expires_at' => 'datetime', + ]; + } + + public function redemptions(): HasMany + { + return $this->hasMany(CouponRedemption::class); + } + + public function isValid(): bool + { + if ($this->expires_at && $this->expires_at->isPast()) { + return false; + } + + if ($this->max_uses !== null && $this->times_used >= $this->max_uses) { + return false; + } + + return true; + } +} diff --git a/website/app/Models/CouponRedemption.php b/website/app/Models/CouponRedemption.php new file mode 100644 index 0000000..401edb3 --- /dev/null +++ b/website/app/Models/CouponRedemption.php @@ -0,0 +1,44 @@ + 'decimal:2', + ]; + } + + public function coupon(): BelongsTo + { + return $this->belongsTo(Coupon::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function subscription(): BelongsTo + { + return $this->belongsTo(Subscription::class); + } +} diff --git a/website/app/Models/Invoice.php b/website/app/Models/Invoice.php new file mode 100644 index 0000000..bc38dba --- /dev/null +++ b/website/app/Models/Invoice.php @@ -0,0 +1,61 @@ + 'decimal:2', + 'tax' => 'decimal:2', + 'due_date' => 'datetime', + 'paid_at' => 'datetime', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function subscription(): BelongsTo + { + return $this->belongsTo(Subscription::class); + } + + public function items(): HasMany + { + return $this->hasMany(InvoiceItem::class); + } + + public function paymentTransactions(): HasMany + { + return $this->hasMany(PaymentTransaction::class); + } +} diff --git a/website/app/Models/InvoiceItem.php b/website/app/Models/InvoiceItem.php new file mode 100644 index 0000000..0e24a72 --- /dev/null +++ b/website/app/Models/InvoiceItem.php @@ -0,0 +1,34 @@ + 'decimal:2', + 'quantity' => 'integer', + ]; + } + + public function invoice(): BelongsTo + { + return $this->belongsTo(Invoice::class); + } +} diff --git a/website/app/Models/PaymentTransaction.php b/website/app/Models/PaymentTransaction.php new file mode 100644 index 0000000..36bedbf --- /dev/null +++ b/website/app/Models/PaymentTransaction.php @@ -0,0 +1,52 @@ + 'decimal:2', + 'metadata' => 'array', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function subscription(): BelongsTo + { + return $this->belongsTo(Subscription::class); + } + + public function invoice(): BelongsTo + { + return $this->belongsTo(Invoice::class); + } +} diff --git a/website/app/Models/Plan.php b/website/app/Models/Plan.php new file mode 100644 index 0000000..edfa7ce --- /dev/null +++ b/website/app/Models/Plan.php @@ -0,0 +1,58 @@ + 'decimal:2', + 'features' => 'array', + 'stock_quantity' => 'integer', + 'sort_order' => 'integer', + ]; + } + + public function services(): HasMany + { + return $this->hasMany(Service::class); + } + + public function isAvailable(): bool + { + if ($this->status !== 'active') { + return false; + } + + if ($this->stock_quantity !== null && $this->stock_quantity <= 0) { + return false; + } + + return true; + } +} diff --git a/website/app/Models/ProvisioningLog.php b/website/app/Models/ProvisioningLog.php new file mode 100644 index 0000000..98a2c44 --- /dev/null +++ b/website/app/Models/ProvisioningLog.php @@ -0,0 +1,47 @@ + 'array', + ]; + } + + public function service(): BelongsTo + { + return $this->belongsTo(Service::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function admin(): BelongsTo + { + return $this->belongsTo(User::class, 'admin_id'); + } +} diff --git a/website/app/Models/Service.php b/website/app/Models/Service.php new file mode 100644 index 0000000..f9857fe --- /dev/null +++ b/website/app/Models/Service.php @@ -0,0 +1,76 @@ + 'encrypted:array', + 'provisioned_at' => 'datetime', + 'suspended_at' => 'datetime', + 'terminated_at' => 'datetime', + 'auto_renew' => 'boolean', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function subscription(): BelongsTo + { + return $this->belongsTo(Subscription::class); + } + + public function plan(): BelongsTo + { + return $this->belongsTo(Plan::class); + } + + public function provisioningLogs(): HasMany + { + return $this->hasMany(ProvisioningLog::class); + } + + public function bandwidthUsage(): HasMany + { + return $this->hasMany(BandwidthUsage::class); + } + + public function isActive(): bool + { + return $this->status === 'active'; + } +} diff --git a/website/app/Models/SupportTicket.php b/website/app/Models/SupportTicket.php new file mode 100644 index 0000000..6173990 --- /dev/null +++ b/website/app/Models/SupportTicket.php @@ -0,0 +1,36 @@ + 'integer', + 'last_reply_at' => 'datetime', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/website/app/Models/User.php b/website/app/Models/User.php index d5d9e92..5fa0a43 100644 --- a/website/app/Models/User.php +++ b/website/app/Models/User.php @@ -1,48 +1,106 @@ */ - use HasFactory, Notifiable; + use Billable, HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable; - /** - * The attributes that are mass assignable. - * - * @var list - */ + /** @var list */ protected $fillable = [ 'name', 'email', 'password', + 'status', + 'phone', + 'company', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ + /** @var list */ protected $hidden = [ 'password', 'remember_token', + 'two_factor_secret', + 'two_factor_recovery_codes', ]; - /** - * Get the attributes that should be cast. - * - * @return array - */ + /** @return array */ protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'passkey_credentials' => 'json', ]; } + + public function profile(): HasOne + { + return $this->hasOne(UserProfile::class); + } + + public function services(): HasMany + { + return $this->hasMany(Service::class); + } + + /** @return HasMany<\App\Models\Invoice, $this> */ + public function invoices(): HasMany + { + return $this->hasMany(Invoice::class); + } + + public function paymentTransactions(): HasMany + { + return $this->hasMany(PaymentTransaction::class); + } + + public function auditLogs(): HasMany + { + return $this->hasMany(AuditLog::class); + } + + public function supportTickets(): HasMany + { + return $this->hasMany(SupportTicket::class); + } + + public function couponRedemptions(): HasMany + { + return $this->hasMany(CouponRedemption::class); + } + + public function isAdmin(): bool + { + return $this->hasRole('admin'); + } + + public function isCustomer(): bool + { + return $this->hasRole('customer'); + } + + public function isSuspended(): bool + { + return $this->status === 'suspended'; + } + + public function isBanned(): bool + { + return $this->status === 'banned'; + } } diff --git a/website/app/Models/UserProfile.php b/website/app/Models/UserProfile.php new file mode 100644 index 0000000..1d5e559 --- /dev/null +++ b/website/app/Models/UserProfile.php @@ -0,0 +1,47 @@ + 'boolean', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/website/app/Providers/AppServiceProvider.php b/website/app/Providers/AppServiceProvider.php index 4d25ed8..0281f47 100644 --- a/website/app/Providers/AppServiceProvider.php +++ b/website/app/Providers/AppServiceProvider.php @@ -1,24 +1,36 @@ app->singleton(LoginResponseContract::class, LoginResponse::class); } - /** - * Bootstrap any application services. - */ public function boot(): void { - // + if ($this->app->environment('production', 'local')) { + URL::forceScheme('https'); + } + + RateLimiter::for('api', function (Request $request) { + return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); + }); + + RateLimiter::for('register', function (Request $request) { + return Limit::perMinute(3)->by($request->ip()); + }); } } diff --git a/website/app/Providers/FortifyServiceProvider.php b/website/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..596347a --- /dev/null +++ b/website/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,54 @@ + Inertia::render('Auth/Login')); + Fortify::registerView(fn () => Inertia::render('Auth/Register')); + Fortify::requestPasswordResetLinkView(fn () => Inertia::render('Auth/ForgotPassword')); + Fortify::resetPasswordView(fn (Request $request) => Inertia::render('Auth/ResetPassword', [ + 'token' => $request->route('token'), + 'email' => $request->query('email'), + ])); + Fortify::verifyEmailView(fn () => Inertia::render('Auth/VerifyEmail')); + Fortify::confirmPasswordView(fn () => Inertia::render('Auth/ConfirmPassword')); + Fortify::twoFactorChallengeView(fn () => Inertia::render('Auth/TwoFactorChallenge')); + + RateLimiter::for('login', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + } +} diff --git a/website/boost.json b/website/boost.json index bb845a0..194b80a 100644 --- a/website/boost.json +++ b/website/boost.json @@ -7,6 +7,7 @@ "mcp": true, "sail": false, "skills": [ - "pest-testing" + "pest-testing", + "tailwindcss-development" ] } diff --git a/website/bootstrap/app.php b/website/bootstrap/app.php index 116b7e2..d711574 100644 --- a/website/bootstrap/app.php +++ b/website/bootstrap/app.php @@ -3,15 +3,41 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Support\Facades\Route; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', + then: function (): void { + Route::domain(config('app.domains.marketing')) + ->middleware('web') + ->group(base_path('routes/marketing.php')); + + Route::domain(config('app.domains.account')) + ->middleware(['web', 'auth', 'verified']) + ->group(base_path('routes/account.php')); + + Route::domain(config('app.domains.admin')) + ->middleware(['web', 'auth', 'verified', 'role:admin']) + ->group(base_path('routes/admin.php')); + }, ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->trustProxies(at: '*'); + + $middleware->web(append: [ + \Inertia\Middleware::class, + ]); + + $middleware->alias([ + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + 'ensure_not_suspended' => \App\Http\Middleware\EnsureUserNotSuspended::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/website/bootstrap/cache/.gitignore b/website/bootstrap/cache/.gitignore old mode 100644 new mode 100755 diff --git a/website/bootstrap/providers.php b/website/bootstrap/providers.php index c7da1af..290f57d 100644 --- a/website/bootstrap/providers.php +++ b/website/bootstrap/providers.php @@ -2,4 +2,5 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\FortifyServiceProvider::class, ]; diff --git a/website/composer.json b/website/composer.json index ce97e2e..d4e3a50 100644 --- a/website/composer.json +++ b/website/composer.json @@ -10,8 +10,14 @@ "license": "MIT", "require": { "php": "^8.2", + "inertiajs/inertia-laravel": "^2.0", + "laravel/cashier": "^16.2", + "laravel/fortify": "^1.34", "laravel/framework": "^12.0", - "laravel/tinker": "^2.10.1" + "laravel/passport": "^13.4", + "laravel/tinker": "^2.10.1", + "spatie/laravel-permission": "^6.24", + "srmklive/paypal": "^3.0" }, "require-dev": { "fakerphp/faker": "^1.23", @@ -89,4 +95,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/website/composer.lock b/website/composer.lock index 7c78d44..7812357 100644 --- a/website/composer.lock +++ b/website/composer.lock @@ -4,8 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9ed7eeda37ced62897a86da77312ea35", + "content-hash": "7166e8eccf92defcc15b9e2bdb8d8108", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" + }, + "time": "2025-11-19T17:15:36+00:00" + }, { "name": "brick/math", "version": "0.14.7", @@ -135,6 +190,123 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -508,6 +680,69 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v7.0.2", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v7.0.2" + }, + "time": "2025-12-16T22:17:28+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.4.0", @@ -1052,6 +1287,227 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "inertiajs/inertia-laravel", + "version": "v2.0.19", + "source": { + "type": "git", + "url": "https://github.com/inertiajs/inertia-laravel.git", + "reference": "732a991342a0f82653a935440e2f3b9be1eb6f6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/732a991342a0f82653a935440e2f3b9be1eb6f6e", + "reference": "732a991342a0f82653a935440e2f3b9be1eb6f6e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^10.0|^11.0|^12.0", + "php": "^8.1.0", + "symfony/console": "^6.2|^7.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.2", + "larastan/larastan": "^3.0", + "laravel/pint": "^1.16", + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^8.0|^9.2|^10.0", + "phpunit/phpunit": "^10.4|^11.5", + "roave/security-advisories": "dev-master" + }, + "suggest": { + "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Inertia\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "./helpers.php" + ], + "psr-4": { + "Inertia\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "homepage": "https://reinink.ca" + } + ], + "description": "The Laravel adapter for Inertia.js.", + "keywords": [ + "inertia", + "laravel" + ], + "support": { + "issues": "https://github.com/inertiajs/inertia-laravel/issues", + "source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.19" + }, + "time": "2026-01-13T15:29:20+00:00" + }, + { + "name": "laravel/cashier", + "version": "v16.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/cashier-stripe.git", + "reference": "9634b60c196ef1a512aa4f9543b6c2a1d64dff85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/cashier-stripe/zipball/9634b60c196ef1a512aa4f9543b6c2a1d64dff85", + "reference": "9634b60c196ef1a512aa4f9543b6c2a1d64dff85", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/log": "^10.0|^11.0|^12.0", + "illuminate/notifications": "^10.0|^11.0|^12.0", + "illuminate/pagination": "^10.0|^11.0|^12.0", + "illuminate/routing": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/view": "^10.0|^11.0|^12.0", + "moneyphp/money": "^4.0", + "nesbot/carbon": "^2.0|^3.0", + "php": "^8.1", + "stripe/stripe-php": "^17.3.0", + "symfony/console": "^6.0|^7.0", + "symfony/http-kernel": "^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.22.1", + "symfony/polyfill-php84": "^1.32" + }, + "require-dev": { + "dompdf/dompdf": "^2.0|^3.0", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "phpstan/phpstan": "^1.10", + "spatie/laravel-ray": "^1.40" + }, + "suggest": { + "dompdf/dompdf": "Required when generating and downloading invoice PDF's using Dompdf (^2.0|^3.0).", + "ext-intl": "Allows for more locales besides the default \"en\" when formatting money values." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Cashier\\CashierServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "16.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Cashier\\": "src/", + "Laravel\\Cashier\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Dries Vints", + "email": "dries@laravel.com" + } + ], + "description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.", + "keywords": [ + "billing", + "laravel", + "stripe" + ], + "support": { + "issues": "https://github.com/laravel/cashier/issues", + "source": "https://github.com/laravel/cashier" + }, + "time": "2026-01-06T16:30:29+00:00" + }, + { + "name": "laravel/fortify", + "version": "v1.34.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "412575e9c0cb21d49a30b7045ad4902019f538c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/412575e9c0cb21d49a30b7045ad4902019f538c2", + "reference": "412575e9c0cb21d49a30b7045ad4902019f538c2", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "pragmarx/google2fa": "^9.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2026-02-03T06:55:55+00:00" + }, { "name": "laravel/framework", "version": "v12.50.0", @@ -1274,6 +1730,81 @@ }, "time": "2026-02-04T18:34:13+00:00" }, + { + "name": "laravel/passport", + "version": "v13.4.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "b3c549d519dbf50be4eae563a267dd1553736041" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/b3c549d519dbf50be4eae563a267dd1553736041", + "reference": "b3c549d519dbf50be4eae563a267dd1553736041", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "firebase/php-jwt": "^6.4|^7.0", + "illuminate/auth": "^11.35|^12.0", + "illuminate/console": "^11.35|^12.0", + "illuminate/container": "^11.35|^12.0", + "illuminate/contracts": "^11.35|^12.0", + "illuminate/cookie": "^11.35|^12.0", + "illuminate/database": "^11.35|^12.0", + "illuminate/encryption": "^11.35|^12.0", + "illuminate/http": "^11.35|^12.0", + "illuminate/support": "^11.35|^12.0", + "league/oauth2-server": "^9.2", + "php": "^8.2", + "php-http/discovery": "^1.20", + "phpseclib/phpseclib": "^3.0", + "psr/http-factory-implementation": "*", + "symfony/console": "^7.1", + "symfony/psr-http-message-bridge": "^7.1" + }, + "require-dev": { + "orchestra/testbench": "^9.15|^10.8", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/", + "Laravel\\Passport\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "support": { + "issues": "https://github.com/laravel/passport/issues", + "source": "https://github.com/laravel/passport" + }, + "time": "2025-12-22T14:58:04+00:00" + }, { "name": "laravel/prompts", "version": "v0.3.12", @@ -1460,6 +1991,143 @@ }, "time": "2025-12-19T19:16:45+00:00" }, + { + "name": "lcobucci/clock", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "a3139d9e97d47826f27e6a17bb63f13621f86058" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/a3139d9e97d47826f27e6a17bb63f13621f86058", + "reference": "a3139d9e97d47826f27e6a17bb63f13621f86058", + "shasum": "" + }, + "require": { + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.31", + "lcobucci/coding-standard": "^11.2.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^2.0.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0.0", + "phpstan/phpstan-strict-rules": "^2.0.0", + "phpunit/phpunit": "^12.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.5.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-10-27T09:03:17+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.6.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.6.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-10-17T11:30:53+00:00" + }, { "name": "league/commonmark", "version": "2.8.0", @@ -1649,6 +2317,65 @@ ], "time": "2022-12-11T20:36:23+00:00" }, + { + "name": "league/event", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "shasum": "" + }, + "require": { + "php": ">=7.2.0", + "psr/event-dispatcher": "^1.0" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.45", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/3.0.3" + }, + "time": "2024-09-04T16:06:53+00:00" + }, { "name": "league/flysystem", "version": "3.31.0", @@ -1837,6 +2564,102 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth2-server", + "version": "9.3.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "d8e2f39f645a82b207bbac441694d6e6079357cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/d8e2f39f645a82b207bbac441694d6e6079357cb", + "reference": "d8e2f39f645a82b207bbac441694d6e6079357cb", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.4", + "ext-json": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.3 || ^3.0", + "lcobucci/jwt": "^5.0", + "league/event": "^3.0", + "league/uri": "^7.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^2.0", + "psr/http-server-middleware": "^1.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "laminas/laminas-diactoros": "^3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.12|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.1.4|^2.0", + "phpstan/phpstan-phpunit": "^1.3.15|^2.0", + "phpstan/phpstan-strict-rules": "^1.5.2|^2.0", + "phpunit/phpunit": "^10.5|^11.5|^12.0", + "roave/security-advisories": "dev-master", + "slevomat/coding-standard": "^8.14.1", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server/issues", + "source": "https://github.com/thephpleague/oauth2-server/tree/9.3.0" + }, + "funding": [ + { + "url": "https://github.com/sephster", + "type": "github" + } + ], + "time": "2025-11-25T22:51:15+00:00" + }, { "name": "league/uri", "version": "7.8.0", @@ -2019,6 +2842,96 @@ ], "time": "2026-01-15T06:54:53+00:00" }, + { + "name": "moneyphp/money", + "version": "v4.8.0", + "source": { + "type": "git", + "url": "https://github.com/moneyphp/money.git", + "reference": "b358727ea5a5cd2d7475e59c31dfc352440ae7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moneyphp/money/zipball/b358727ea5a5cd2d7475e59c31dfc352440ae7ec", + "reference": "b358727ea5a5cd2d7475e59c31dfc352440ae7ec", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-filter": "*", + "ext-json": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cache/taggable-cache": "^1.1.0", + "doctrine/coding-standard": "^12.0", + "doctrine/instantiator": "^1.5.0 || ^2.0", + "ext-gmp": "*", + "ext-intl": "*", + "florianv/exchanger": "^2.8.1", + "florianv/swap": "^4.3.0", + "moneyphp/crypto-currencies": "^1.1.0", + "moneyphp/iso-currencies": "^3.4", + "php-http/message": "^1.16.0", + "php-http/mock-client": "^1.6.0", + "phpbench/phpbench": "^1.2.5", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.9", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.9", + "psr/cache": "^1.0.1 || ^2.0 || ^3.0", + "ticketswap/phpstan-error-formatter": "^1.1" + }, + "suggest": { + "ext-gmp": "Calculate without integer limits", + "ext-intl": "Format Money objects with intl", + "florianv/exchanger": "Exchange rates library for PHP", + "florianv/swap": "Exchange rates library for PHP", + "psr/cache-implementation": "Used for Currency caching" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Money\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "http://verraes.net" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" + } + ], + "description": "PHP implementation of Fowler's Money pattern", + "homepage": "http://moneyphp.org", + "keywords": [ + "Value Object", + "money", + "vo" + ], + "support": { + "issues": "https://github.com/moneyphp/money/issues", + "source": "https://github.com/moneyphp/money/tree/v4.8.0" + }, + "time": "2025-10-23T07:55:09+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -2526,6 +3439,204 @@ ], "time": "2025-11-20T02:34:59+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", @@ -2601,6 +3712,168 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.49", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-01-27T09:17:28+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v9.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + }, + "time": "2025-09-19T22:51:08+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -2912,6 +4185,119 @@ }, "time": "2023-04-04T09:54:51+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -3290,6 +4676,214 @@ }, "time": "2025-12-14T04:43:48+00:00" }, + { + "name": "spatie/laravel-permission", + "version": "6.24.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "76adb1fc8d07c16a0721c35c4cc330b7a12598d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/76adb1fc8d07c16a0721c35c4cc330b7a12598d7", + "reference": "76adb1fc8d07c16a0721c35c4cc330b7a12598d7", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.4|^10.1|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 8.0 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/6.24.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-12-13T21:45:21+00:00" + }, + { + "name": "srmklive/paypal", + "version": "3.0.40", + "source": { + "type": "git", + "url": "https://github.com/srmklive/laravel-paypal.git", + "reference": "1ddc49fd836a4785933ab953452152f3fedbac63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/srmklive/laravel-paypal/zipball/1ddc49fd836a4785933ab953452152f3fedbac63", + "reference": "1ddc49fd836a4785933ab953452152f3fedbac63", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "guzzlehttp/guzzle": "~7.0", + "illuminate/support": "~6.0|~7.0|~8.0|~9.0|^10.0|^11.0|^12.0", + "nesbot/carbon": "~2.0|^3.0", + "php": ">=7.2|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.0", + "symfony/var-dumper": "~5.0|^6.0|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PayPal": "Srmklive\\PayPal\\Facades\\PayPal" + }, + "providers": [ + "Srmklive\\PayPal\\Providers\\PayPalServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Srmklive\\PayPal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raza Mehdi", + "email": "srmk@outlook.com" + } + ], + "description": "Laravel plugin For Processing Payments Through Paypal Express Checkout. Can Be Used Independently With Other Applications.", + "keywords": [ + "http", + "laravel paypal", + "paypal", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/srmklive/laravel-paypal/issues", + "source": "https://github.com/srmklive/laravel-paypal/tree/3.0.40" + }, + "time": "2025-02-25T21:38:18+00:00" + }, + { + "name": "stripe/stripe-php", + "version": "v17.6.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/a6219df5df1324a0d3f1da25fb5e4b8a3307ea16", + "reference": "a6219df5df1324a0d3f1da25fb5e4b8a3307ea16", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.72.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v17.6.0" + }, + "time": "2025-08-27T19:32:42+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", @@ -4452,6 +6046,94 @@ ], "time": "2025-06-27T09:58:17+00:00" }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-20T22:24:30+00:00" + }, { "name": "symfony/polyfill-intl-idn", "version": "v1.33.0", @@ -5181,6 +6863,94 @@ ], "time": "2026-01-26T15:07:59+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/929ffe10bbfbb92e711ac3818d416f9daffee067", + "reference": "929ffe10bbfbb92e711ac3818d416f9daffee067", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-03T23:30:35+00:00" + }, { "name": "symfony/routing", "version": "v7.4.4", @@ -9564,5 +11334,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/website/config/app.php b/website/config/app.php index 1e1baf8..2ce14a3 100644 --- a/website/config/app.php +++ b/website/config/app.php @@ -123,4 +123,20 @@ return [ 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], + /* + |-------------------------------------------------------------------------- + | Domain Routing + |-------------------------------------------------------------------------- + | + | These values define the domains for the multi-domain routing setup. + | Each subdomain serves a different section of the application. + | + */ + + 'domains' => [ + 'marketing' => env('DOMAIN_MARKETING', 'ezscale.dev'), + 'account' => env('DOMAIN_ACCOUNT', 'account.ezscale.dev'), + 'admin' => env('DOMAIN_ADMIN', 'admin.ezscale.dev'), + ], + ]; diff --git a/website/config/auth.php b/website/config/auth.php index 28e9d9c..b48f6f6 100644 --- a/website/config/auth.php +++ b/website/config/auth.php @@ -40,6 +40,11 @@ return [ 'driver' => 'session', 'provider' => 'users', ], + + 'api' => [ + 'driver' => 'passport', + 'provider' => 'users', + ], ], /* diff --git a/website/config/cashier.php b/website/config/cashier.php new file mode 100644 index 0000000..1c99dda --- /dev/null +++ b/website/config/cashier.php @@ -0,0 +1,127 @@ + env('STRIPE_KEY'), + + 'secret' => env('STRIPE_SECRET'), + + /* + |-------------------------------------------------------------------------- + | Cashier Path + |-------------------------------------------------------------------------- + | + | This is the base URI path where Cashier's views, such as the payment + | verification screen, will be available from. You're free to tweak + | this path according to your preferences and application design. + | + */ + + 'path' => env('CASHIER_PATH', 'stripe'), + + /* + |-------------------------------------------------------------------------- + | Stripe Webhooks + |-------------------------------------------------------------------------- + | + | Your Stripe webhook secret is used to prevent unauthorized requests to + | your Stripe webhook handling controllers. The tolerance setting will + | check the drift between the current time and the signed request's. + | + */ + + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET'), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + 'events' => WebhookCommand::DEFAULT_EVENTS, + ], + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | This is the default currency that will be used when generating charges + | from your application. Of course, you are welcome to use any of the + | various world currencies that are currently supported via Stripe. + | + */ + + 'currency' => env('CASHIER_CURRENCY', 'usd'), + + /* + |-------------------------------------------------------------------------- + | Currency Locale + |-------------------------------------------------------------------------- + | + | This is the default locale in which your money values are formatted in + | for display. To utilize other locales besides the default en locale + | verify you have the "intl" PHP extension installed on the system. + | + */ + + 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Payment Confirmation Notification + |-------------------------------------------------------------------------- + | + | If this setting is enabled, Cashier will automatically notify customers + | whose payments require additional verification. You should listen to + | Stripe's webhooks in order for this feature to function correctly. + | + */ + + 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), + + /* + |-------------------------------------------------------------------------- + | Invoice Settings + |-------------------------------------------------------------------------- + | + | The following options determine how Cashier invoices are converted from + | HTML into PDFs. You're free to change the options based on the needs + | of your application or your preferences regarding invoice styling. + | + */ + + 'invoices' => [ + 'renderer' => env('CASHIER_INVOICE_RENDERER', DompdfInvoiceRenderer::class), + + 'options' => [ + // Supported: 'letter', 'legal', 'A4' + 'paper' => env('CASHIER_PAPER', 'letter'), + + 'remote_enabled' => env('CASHIER_REMOTE_ENABLED', false), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Stripe Logger + |-------------------------------------------------------------------------- + | + | This setting defines which logging channel will be used by the Stripe + | library to write log messages. You are free to specify any of your + | logging channels listed inside the "logging" configuration file. + | + */ + + 'logger' => env('CASHIER_LOGGER'), + +]; diff --git a/website/config/fortify.php b/website/config/fortify.php new file mode 100644 index 0000000..22b5078 --- /dev/null +++ b/website/config/fortify.php @@ -0,0 +1,159 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/dashboard', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => env('FORTIFY_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => true, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + ], + +]; diff --git a/website/config/passport.php b/website/config/passport.php new file mode 100644 index 0000000..72cfb7b --- /dev/null +++ b/website/config/passport.php @@ -0,0 +1,46 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Encryption Keys + |-------------------------------------------------------------------------- + | + | Passport uses encryption keys while generating secure access tokens for + | your application. By default, the keys are stored as local files but + | can be set via environment variables when that is more convenient. + | + */ + + 'private_key' => env('PASSPORT_PRIVATE_KEY'), + + 'public_key' => env('PASSPORT_PUBLIC_KEY'), + + /* + |-------------------------------------------------------------------------- + | Passport Database Connection + |-------------------------------------------------------------------------- + | + | By default, Passport's models will utilize your application's default + | database connection. If you wish to use a different connection you + | may specify the configured name of the database connection here. + | + */ + + 'connection' => env('PASSPORT_CONNECTION'), + +]; diff --git a/website/config/paypal.php b/website/config/paypal.php new file mode 100644 index 0000000..500e563 --- /dev/null +++ b/website/config/paypal.php @@ -0,0 +1,26 @@ +. + */ + +return [ + 'mode' => env('PAYPAL_MODE', 'sandbox'), // Can only be 'sandbox' Or 'live'. If empty or invalid, 'live' will be used. + 'sandbox' => [ + 'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''), + 'client_secret' => env('PAYPAL_SANDBOX_CLIENT_SECRET', ''), + 'app_id' => 'APP-80W284485P519543T', + ], + 'live' => [ + 'client_id' => env('PAYPAL_LIVE_CLIENT_ID', ''), + 'client_secret' => env('PAYPAL_LIVE_CLIENT_SECRET', ''), + 'app_id' => env('PAYPAL_LIVE_APP_ID', ''), + ], + + 'payment_action' => env('PAYPAL_PAYMENT_ACTION', 'Sale'), // Can only be 'Sale', 'Authorization' or 'Order' + 'currency' => env('PAYPAL_CURRENCY', 'USD'), + 'notify_url' => env('PAYPAL_NOTIFY_URL', ''), // Change this accordingly for your application. + 'locale' => env('PAYPAL_LOCALE', 'en_US'), // force gateway language i.e. it_IT, es_ES, en_US ... (for express checkout only) + 'validate_ssl' => env('PAYPAL_VALIDATE_SSL', true), // Validate SSL when creating api client. +]; diff --git a/website/config/permission.php b/website/config/permission.php new file mode 100644 index 0000000..b6c88e2 --- /dev/null +++ b/website/config/permission.php @@ -0,0 +1,202 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Spatie\Permission\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Spatie\Permission\Models\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/website/config/queue.php b/website/config/queue.php index d133301..294c5c6 100644 --- a/website/config/queue.php +++ b/website/config/queue.php @@ -103,7 +103,7 @@ return [ */ 'batching' => [ - 'database' => env('DB_CONNECTION', 'sqlite'), + 'database' => env('DB_CONNECTION', 'mysql'), 'table' => 'job_batches', ], @@ -122,7 +122,7 @@ return [ 'failed' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), - 'database' => env('DB_CONNECTION', 'sqlite'), + 'database' => env('DB_CONNECTION', 'mysql'), 'table' => 'failed_jobs', ], diff --git a/website/database/factories/AnnouncementFactory.php b/website/database/factories/AnnouncementFactory.php new file mode 100644 index 0000000..48469b4 --- /dev/null +++ b/website/database/factories/AnnouncementFactory.php @@ -0,0 +1,22 @@ + */ +class AnnouncementFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'title' => fake()->sentence(), + 'content' => fake()->paragraphs(2, true), + 'type' => fake()->randomElement(['maintenance', 'feature', 'outage']), + 'published_at' => now(), + ]; + } +} diff --git a/website/database/factories/AuditLogFactory.php b/website/database/factories/AuditLogFactory.php new file mode 100644 index 0000000..efb465a --- /dev/null +++ b/website/database/factories/AuditLogFactory.php @@ -0,0 +1,23 @@ + */ +class AuditLogFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'action' => fake()->randomElement(['login', 'logout', 'service_provisioned', 'payment_failed']), + 'ip_address' => fake()->ipv4(), + 'user_agent' => fake()->userAgent(), + ]; + } +} diff --git a/website/database/factories/CouponFactory.php b/website/database/factories/CouponFactory.php new file mode 100644 index 0000000..1a9cb6e --- /dev/null +++ b/website/database/factories/CouponFactory.php @@ -0,0 +1,25 @@ + */ +class CouponFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'code' => Str::upper(fake()->unique()->bothify('??##??')), + 'type' => fake()->randomElement(['percentage', 'fixed_amount']), + 'value' => fake()->randomFloat(2, 5, 50), + 'max_uses' => fake()->optional()->numberBetween(10, 1000), + 'times_used' => 0, + 'expires_at' => fake()->optional()->dateTimeBetween('now', '+1 year'), + ]; + } +} diff --git a/website/database/factories/InvoiceFactory.php b/website/database/factories/InvoiceFactory.php new file mode 100644 index 0000000..df99dc4 --- /dev/null +++ b/website/database/factories/InvoiceFactory.php @@ -0,0 +1,35 @@ + */ +class InvoiceFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'gateway' => 'stripe', + 'number' => 'INV-'.fake()->unique()->numerify('######'), + 'total' => fake()->randomFloat(2, 5, 500), + 'tax' => fake()->randomFloat(2, 0, 50), + 'currency' => 'USD', + 'status' => fake()->randomElement(['draft', 'sent', 'paid', 'overdue']), + 'due_date' => fake()->dateTimeBetween('now', '+30 days'), + ]; + } + + public function paid(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => 'paid', + 'paid_at' => now(), + ]); + } +} diff --git a/website/database/factories/PlanFactory.php b/website/database/factories/PlanFactory.php new file mode 100644 index 0000000..27f9f80 --- /dev/null +++ b/website/database/factories/PlanFactory.php @@ -0,0 +1,36 @@ + */ +class PlanFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + $name = fake()->unique()->words(2, true); + + return [ + 'name' => ucwords($name), + 'slug' => Str::slug($name), + 'description' => fake()->sentence(), + 'service_type' => fake()->randomElement(['vps', 'dedicated', 'hosting', 'game_server']), + 'price' => fake()->randomFloat(2, 5, 500), + 'currency' => 'USD', + 'billing_cycle' => fake()->randomElement(['monthly', 'quarterly', 'annual']), + 'features' => [ + 'cpu' => fake()->numberBetween(1, 16).' vCPU', + 'ram' => fake()->randomElement(['1GB', '2GB', '4GB', '8GB', '16GB', '32GB']), + 'disk' => fake()->randomElement(['20GB', '50GB', '100GB', '200GB', '500GB']), + 'bandwidth' => fake()->randomElement(['1TB', '2TB', '5TB', '10TB', 'Unlimited']), + ], + 'status' => 'active', + 'sort_order' => fake()->numberBetween(0, 100), + ]; + } +} diff --git a/website/database/factories/ServiceFactory.php b/website/database/factories/ServiceFactory.php new file mode 100644 index 0000000..e503ebe --- /dev/null +++ b/website/database/factories/ServiceFactory.php @@ -0,0 +1,54 @@ + */ +class ServiceFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + $serviceType = fake()->randomElement(['vps', 'dedicated', 'hosting', 'game_server']); + $platforms = [ + 'vps' => 'virtfusion', + 'dedicated' => 'synergycp', + 'hosting' => 'enhance', + 'game_server' => 'pterodactyl', + ]; + + return [ + 'user_id' => User::factory(), + 'plan_id' => Plan::factory(), + 'service_type' => $serviceType, + 'platform' => $platforms[$serviceType], + 'platform_service_id' => (string) fake()->numberBetween(1000, 99999), + 'status' => 'active', + 'ipv4_address' => fake()->ipv4(), + 'hostname' => fake()->domainName(), + 'provisioned_at' => now(), + 'auto_renew' => true, + ]; + } + + public function pending(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => 'pending', + 'provisioned_at' => null, + ]); + } + + public function suspended(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => 'suspended', + 'suspended_at' => now(), + ]); + } +} diff --git a/website/database/factories/UserFactory.php b/website/database/factories/UserFactory.php index 99bb384..b757631 100644 --- a/website/database/factories/UserFactory.php +++ b/website/database/factories/UserFactory.php @@ -1,5 +1,7 @@ - */ + /** @return array */ public function definition(): array { return [ @@ -29,16 +24,44 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'status' => 'active', + 'phone' => fake()->optional()->phoneNumber(), + 'company' => fake()->optional()->company(), ]; } - /** - * Indicate that the model's email address should be unverified. - */ public function unverified(): static { - return $this->state(fn (array $attributes) => [ + return $this->state(fn (array $attributes): array => [ 'email_verified_at' => null, ]); } + + public function admin(): static + { + return $this->afterCreating(function ($user): void { + $user->assignRole('admin'); + }); + } + + public function customer(): static + { + return $this->afterCreating(function ($user): void { + $user->assignRole('customer'); + }); + } + + public function suspended(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => 'suspended', + ]); + } + + public function banned(): static + { + return $this->state(fn (array $attributes): array => [ + 'status' => 'banned', + ]); + } } diff --git a/website/database/factories/UserProfileFactory.php b/website/database/factories/UserProfileFactory.php new file mode 100644 index 0000000..fda8da4 --- /dev/null +++ b/website/database/factories/UserProfileFactory.php @@ -0,0 +1,26 @@ + */ +class UserProfileFactory extends Factory +{ + /** @return array */ + public function definition(): array + { + return [ + 'user_id' => User::factory(), + 'billing_address_line1' => fake()->streetAddress(), + 'billing_city' => fake()->city(), + 'billing_state' => fake()->stateAbbr(), + 'billing_zip' => fake()->postcode(), + 'billing_country' => fake()->countryCode(), + 'tax_exempt' => false, + ]; + } +} diff --git a/website/database/migrations/2026_02_09_070035_add_two_factor_columns_to_users_table.php b/website/database/migrations/2026_02_09_070035_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..11393ef --- /dev/null +++ b/website/database/migrations/2026_02_09_070035_add_two_factor_columns_to_users_table.php @@ -0,0 +1,42 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/website/database/migrations/2026_02_09_070038_create_oauth_auth_codes_table.php b/website/database/migrations/2026_02_09_070038_create_oauth_auth_codes_table.php new file mode 100644 index 0000000..f57fdea --- /dev/null +++ b/website/database/migrations/2026_02_09_070038_create_oauth_auth_codes_table.php @@ -0,0 +1,39 @@ +char('id', 80)->primary(); + $table->foreignId('user_id')->index(); + $table->foreignUuid('client_id'); + $table->text('scopes')->nullable(); + $table->boolean('revoked'); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_auth_codes'); + } + + /** + * Get the migration connection name. + */ + public function getConnection(): ?string + { + return $this->connection ?? config('passport.connection'); + } +}; diff --git a/website/database/migrations/2026_02_09_070039_create_oauth_access_tokens_table.php b/website/database/migrations/2026_02_09_070039_create_oauth_access_tokens_table.php new file mode 100644 index 0000000..4c7eebb --- /dev/null +++ b/website/database/migrations/2026_02_09_070039_create_oauth_access_tokens_table.php @@ -0,0 +1,41 @@ +char('id', 80)->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->foreignUuid('client_id'); + $table->string('name')->nullable(); + $table->text('scopes')->nullable(); + $table->boolean('revoked'); + $table->timestamps(); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_access_tokens'); + } + + /** + * Get the migration connection name. + */ + public function getConnection(): ?string + { + return $this->connection ?? config('passport.connection'); + } +}; diff --git a/website/database/migrations/2026_02_09_070040_create_customer_columns.php b/website/database/migrations/2026_02_09_070040_create_customer_columns.php new file mode 100644 index 0000000..051a761 --- /dev/null +++ b/website/database/migrations/2026_02_09_070040_create_customer_columns.php @@ -0,0 +1,40 @@ +string('stripe_id')->nullable()->index(); + $table->string('pm_type')->nullable(); + $table->string('pm_last_four', 4)->nullable(); + $table->timestamp('trial_ends_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex([ + 'stripe_id', + ]); + + $table->dropColumn([ + 'stripe_id', + 'pm_type', + 'pm_last_four', + 'trial_ends_at', + ]); + }); + } +}; diff --git a/website/database/migrations/2026_02_09_070040_create_oauth_refresh_tokens_table.php b/website/database/migrations/2026_02_09_070040_create_oauth_refresh_tokens_table.php new file mode 100644 index 0000000..00876c8 --- /dev/null +++ b/website/database/migrations/2026_02_09_070040_create_oauth_refresh_tokens_table.php @@ -0,0 +1,37 @@ +char('id', 80)->primary(); + $table->char('access_token_id', 80)->index(); + $table->boolean('revoked'); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_refresh_tokens'); + } + + /** + * Get the migration connection name. + */ + public function getConnection(): ?string + { + return $this->connection ?? config('passport.connection'); + } +}; diff --git a/website/database/migrations/2026_02_09_070041_create_oauth_clients_table.php b/website/database/migrations/2026_02_09_070041_create_oauth_clients_table.php new file mode 100644 index 0000000..bc476c0 --- /dev/null +++ b/website/database/migrations/2026_02_09_070041_create_oauth_clients_table.php @@ -0,0 +1,42 @@ +uuid('id')->primary(); + $table->nullableMorphs('owner'); + $table->string('name'); + $table->string('secret')->nullable(); + $table->string('provider')->nullable(); + $table->text('redirect_uris'); + $table->text('grant_types'); + $table->boolean('revoked'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_clients'); + } + + /** + * Get the migration connection name. + */ + public function getConnection(): ?string + { + return $this->connection ?? config('passport.connection'); + } +}; diff --git a/website/database/migrations/2026_02_09_070041_create_permission_tables.php b/website/database/migrations/2026_02_09_070041_create_permission_tables.php new file mode 100644 index 0000000..9eebf62 --- /dev/null +++ b/website/database/migrations/2026_02_09_070041_create_permission_tables.php @@ -0,0 +1,134 @@ +engine('InnoDB'); + $table->bigIncrements('id'); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + // $table->engine('InnoDB'); + $table->bigIncrements('id'); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/website/database/migrations/2026_02_09_070041_create_subscriptions_table.php b/website/database/migrations/2026_02_09_070041_create_subscriptions_table.php new file mode 100644 index 0000000..ac88319 --- /dev/null +++ b/website/database/migrations/2026_02_09_070041_create_subscriptions_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('user_id'); + $table->string('type'); + $table->string('stripe_id')->unique(); + $table->string('stripe_status'); + $table->string('stripe_price')->nullable(); + $table->integer('quantity')->nullable(); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + + $table->index(['user_id', 'stripe_status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; diff --git a/website/database/migrations/2026_02_09_070042_create_oauth_device_codes_table.php b/website/database/migrations/2026_02_09_070042_create_oauth_device_codes_table.php new file mode 100644 index 0000000..5ccf29c --- /dev/null +++ b/website/database/migrations/2026_02_09_070042_create_oauth_device_codes_table.php @@ -0,0 +1,42 @@ +char('id', 80)->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->foreignUuid('client_id')->index(); + $table->char('user_code', 8)->unique(); + $table->text('scopes'); + $table->boolean('revoked'); + $table->dateTime('user_approved_at')->nullable(); + $table->dateTime('last_polled_at')->nullable(); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_device_codes'); + } + + /** + * Get the migration connection name. + */ + public function getConnection(): ?string + { + return $this->connection ?? config('passport.connection'); + } +}; diff --git a/website/database/migrations/2026_02_09_070042_create_subscription_items_table.php b/website/database/migrations/2026_02_09_070042_create_subscription_items_table.php new file mode 100644 index 0000000..18878bc --- /dev/null +++ b/website/database/migrations/2026_02_09_070042_create_subscription_items_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('subscription_id'); + $table->string('stripe_id')->unique(); + $table->string('stripe_product'); + $table->string('stripe_price'); + $table->integer('quantity')->nullable(); + $table->timestamps(); + + $table->index(['subscription_id', 'stripe_price']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscription_items'); + } +}; diff --git a/website/database/migrations/2026_02_09_070043_add_meter_id_to_subscription_items_table.php b/website/database/migrations/2026_02_09_070043_add_meter_id_to_subscription_items_table.php new file mode 100644 index 0000000..e72aa39 --- /dev/null +++ b/website/database/migrations/2026_02_09_070043_add_meter_id_to_subscription_items_table.php @@ -0,0 +1,28 @@ +string('meter_id')->nullable()->after('stripe_price'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscription_items', function (Blueprint $table) { + $table->dropColumn('meter_id'); + }); + } +}; diff --git a/website/database/migrations/2026_02_09_070044_add_meter_event_name_to_subscription_items_table.php b/website/database/migrations/2026_02_09_070044_add_meter_event_name_to_subscription_items_table.php new file mode 100644 index 0000000..b743b9d --- /dev/null +++ b/website/database/migrations/2026_02_09_070044_add_meter_event_name_to_subscription_items_table.php @@ -0,0 +1,28 @@ +string('meter_event_name')->nullable()->after('quantity'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('subscription_items', function (Blueprint $table) { + $table->dropColumn('meter_event_name'); + }); + } +}; diff --git a/website/database/migrations/2026_02_09_080000_add_custom_columns_to_users_table.php b/website/database/migrations/2026_02_09_080000_add_custom_columns_to_users_table.php new file mode 100644 index 0000000..b722f54 --- /dev/null +++ b/website/database/migrations/2026_02_09_080000_add_custom_columns_to_users_table.php @@ -0,0 +1,27 @@ +string('status')->default('active')->after('password'); + $table->string('phone')->nullable()->after('status'); + $table->string('company')->nullable()->after('phone'); + $table->json('passkey_credentials')->nullable()->after('two_factor_recovery_codes'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn(['status', 'phone', 'company', 'passkey_credentials']); + }); + } +}; diff --git a/website/database/migrations/2026_02_09_080001_create_user_profiles_table.php b/website/database/migrations/2026_02_09_080001_create_user_profiles_table.php new file mode 100644 index 0000000..218a471 --- /dev/null +++ b/website/database/migrations/2026_02_09_080001_create_user_profiles_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('billing_address_line1')->nullable(); + $table->string('billing_address_line2')->nullable(); + $table->string('billing_city')->nullable(); + $table->string('billing_state')->nullable(); + $table->string('billing_zip')->nullable(); + $table->string('billing_country', 2)->nullable(); + $table->string('shipping_address_line1')->nullable(); + $table->string('shipping_address_line2')->nullable(); + $table->string('shipping_city')->nullable(); + $table->string('shipping_state')->nullable(); + $table->string('shipping_zip')->nullable(); + $table->string('shipping_country', 2)->nullable(); + $table->string('tax_id')->nullable(); + $table->boolean('tax_exempt')->default(false); + $table->string('company_name')->nullable(); + $table->string('company_vat')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('user_profiles'); + } +}; diff --git a/website/database/migrations/2026_02_09_080002_create_plans_table.php b/website/database/migrations/2026_02_09_080002_create_plans_table.php new file mode 100644 index 0000000..08973cc --- /dev/null +++ b/website/database/migrations/2026_02_09_080002_create_plans_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->string('service_type'); + $table->decimal('price', 10, 2); + $table->string('currency', 3)->default('USD'); + $table->string('billing_cycle'); + $table->string('stripe_price_id')->nullable()->index(); + $table->string('paypal_plan_id')->nullable()->index(); + $table->json('features')->nullable(); + $table->integer('stock_quantity')->nullable(); + $table->string('status')->default('active'); + $table->integer('sort_order')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('plans'); + } +}; diff --git a/website/database/migrations/2026_02_09_080003_add_custom_columns_to_subscriptions_table.php b/website/database/migrations/2026_02_09_080003_add_custom_columns_to_subscriptions_table.php new file mode 100644 index 0000000..461b387 --- /dev/null +++ b/website/database/migrations/2026_02_09_080003_add_custom_columns_to_subscriptions_table.php @@ -0,0 +1,41 @@ +foreignId('plan_id')->nullable()->constrained()->after('type'); + $table->string('gateway')->default('stripe')->after('plan_id'); + $table->string('gateway_subscription_id')->nullable()->after('gateway'); + $table->string('gateway_customer_id')->nullable()->after('gateway_subscription_id'); + $table->string('gateway_price_id')->nullable()->after('gateway_customer_id'); + $table->timestamp('current_period_start')->nullable()->after('gateway_price_id'); + $table->timestamp('current_period_end')->nullable()->after('current_period_start'); + $table->timestamp('cancelled_at')->nullable()->after('current_period_end'); + }); + } + + public function down(): void + { + Schema::table('subscriptions', function (Blueprint $table): void { + $table->dropForeign(['plan_id']); + $table->dropColumn([ + 'plan_id', + 'gateway', + 'gateway_subscription_id', + 'gateway_customer_id', + 'gateway_price_id', + 'current_period_start', + 'current_period_end', + 'cancelled_at', + ]); + }); + } +}; diff --git a/website/database/migrations/2026_02_09_080004_create_invoices_table.php b/website/database/migrations/2026_02_09_080004_create_invoices_table.php new file mode 100644 index 0000000..010d830 --- /dev/null +++ b/website/database/migrations/2026_02_09_080004_create_invoices_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('subscription_id')->nullable()->constrained()->nullOnDelete(); + $table->string('gateway'); + $table->string('gateway_invoice_id')->nullable()->index(); + $table->string('number')->unique(); + $table->decimal('total', 10, 2); + $table->decimal('tax', 10, 2)->default(0); + $table->string('currency', 3)->default('USD'); + $table->string('status')->default('draft'); + $table->string('invoice_pdf')->nullable(); + $table->timestamp('due_date')->nullable(); + $table->timestamp('paid_at')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('invoices'); + } +}; diff --git a/website/database/migrations/2026_02_09_080005_create_invoice_items_table.php b/website/database/migrations/2026_02_09_080005_create_invoice_items_table.php new file mode 100644 index 0000000..b123d88 --- /dev/null +++ b/website/database/migrations/2026_02_09_080005_create_invoice_items_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('invoice_id')->constrained()->cascadeOnDelete(); + $table->string('description'); + $table->decimal('amount', 10, 2); + $table->integer('quantity')->default(1); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('invoice_items'); + } +}; diff --git a/website/database/migrations/2026_02_09_080006_create_payment_transactions_table.php b/website/database/migrations/2026_02_09_080006_create_payment_transactions_table.php new file mode 100644 index 0000000..8b3d773 --- /dev/null +++ b/website/database/migrations/2026_02_09_080006_create_payment_transactions_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('subscription_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('invoice_id')->nullable()->constrained()->nullOnDelete(); + $table->string('gateway'); + $table->string('gateway_transaction_id')->nullable()->index(); + $table->decimal('amount', 10, 2); + $table->string('currency', 3)->default('USD'); + $table->string('status'); + $table->string('payment_method')->nullable(); + $table->string('description')->nullable(); + $table->json('metadata')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('payment_transactions'); + } +}; diff --git a/website/database/migrations/2026_02_09_080007_create_coupons_table.php b/website/database/migrations/2026_02_09_080007_create_coupons_table.php new file mode 100644 index 0000000..9d7bee1 --- /dev/null +++ b/website/database/migrations/2026_02_09_080007_create_coupons_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('code')->unique(); + $table->string('type'); + $table->decimal('value', 10, 2); + $table->string('currency', 3)->nullable(); + $table->json('applies_to')->nullable(); + $table->integer('max_uses')->nullable(); + $table->integer('times_used')->default(0); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('coupons'); + } +}; diff --git a/website/database/migrations/2026_02_09_080008_create_coupon_redemptions_table.php b/website/database/migrations/2026_02_09_080008_create_coupon_redemptions_table.php new file mode 100644 index 0000000..6e27d73 --- /dev/null +++ b/website/database/migrations/2026_02_09_080008_create_coupon_redemptions_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('coupon_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('subscription_id')->nullable()->constrained()->nullOnDelete(); + $table->decimal('discount_amount', 10, 2); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('coupon_redemptions'); + } +}; diff --git a/website/database/migrations/2026_02_09_080009_create_services_table.php b/website/database/migrations/2026_02_09_080009_create_services_table.php new file mode 100644 index 0000000..3e55978 --- /dev/null +++ b/website/database/migrations/2026_02_09_080009_create_services_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('subscription_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('plan_id')->constrained(); + $table->string('service_type'); + $table->string('platform'); + $table->string('platform_service_id')->nullable()->index(); + $table->string('status')->default('pending'); + $table->string('ipv4_address')->nullable(); + $table->string('ipv6_address')->nullable(); + $table->string('hostname')->nullable(); + $table->string('domain')->nullable(); + $table->text('credentials')->nullable(); + $table->timestamp('provisioned_at')->nullable(); + $table->timestamp('suspended_at')->nullable(); + $table->timestamp('terminated_at')->nullable(); + $table->boolean('auto_renew')->default(true); + $table->timestamps(); + + $table->index('user_id'); + $table->index('status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('services'); + } +}; diff --git a/website/database/migrations/2026_02_09_080010_create_provisioning_logs_table.php b/website/database/migrations/2026_02_09_080010_create_provisioning_logs_table.php new file mode 100644 index 0000000..349924e --- /dev/null +++ b/website/database/migrations/2026_02_09_080010_create_provisioning_logs_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('service_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->string('action'); + $table->string('platform'); + $table->json('platform_response')->nullable(); + $table->string('status')->default('pending'); + $table->text('error_message')->nullable(); + $table->foreignId('admin_id')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('provisioning_logs'); + } +}; diff --git a/website/database/migrations/2026_02_09_080011_create_bandwidth_usage_table.php b/website/database/migrations/2026_02_09_080011_create_bandwidth_usage_table.php new file mode 100644 index 0000000..5190f5d --- /dev/null +++ b/website/database/migrations/2026_02_09_080011_create_bandwidth_usage_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('service_id')->constrained()->cascadeOnDelete(); + $table->timestamp('period_start'); + $table->timestamp('period_end'); + $table->unsignedBigInteger('bytes_in')->default(0); + $table->unsignedBigInteger('bytes_out')->default(0); + $table->unsignedBigInteger('total_bytes')->default(0); + $table->unsignedBigInteger('quota_bytes')->default(0); + $table->unsignedBigInteger('overage_bytes')->default(0); + $table->decimal('overage_charge', 10, 2)->default(0); + $table->string('source')->default('elastiflow'); + $table->timestamps(); + + $table->index(['service_id', 'period_start']); + }); + } + + public function down(): void + { + Schema::dropIfExists('bandwidth_usage'); + } +}; diff --git a/website/database/migrations/2026_02_09_080012_create_audit_logs_table.php b/website/database/migrations/2026_02_09_080012_create_audit_logs_table.php new file mode 100644 index 0000000..608afab --- /dev/null +++ b/website/database/migrations/2026_02_09_080012_create_audit_logs_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('admin_id')->nullable()->constrained('users')->nullOnDelete(); + $table->string('action'); + $table->string('resource_type')->nullable(); + $table->unsignedBigInteger('resource_id')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->json('changes')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + $table->index('admin_id'); + $table->index(['resource_type', 'resource_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('audit_logs'); + } +}; diff --git a/website/database/migrations/2026_02_09_080013_create_support_tickets_table.php b/website/database/migrations/2026_02_09_080013_create_support_tickets_table.php new file mode 100644 index 0000000..4140a70 --- /dev/null +++ b/website/database/migrations/2026_02_09_080013_create_support_tickets_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->unsignedBigInteger('supportpal_ticket_id')->nullable()->unique(); + $table->string('subject'); + $table->string('status')->default('open'); + $table->string('priority')->default('medium'); + $table->timestamp('last_reply_at')->nullable(); + $table->timestamps(); + + $table->index('user_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('support_tickets'); + } +}; diff --git a/website/database/migrations/2026_02_09_080014_create_announcements_table.php b/website/database/migrations/2026_02_09_080014_create_announcements_table.php new file mode 100644 index 0000000..9ae112f --- /dev/null +++ b/website/database/migrations/2026_02_09_080014_create_announcements_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('title'); + $table->text('content'); + $table->string('type')->default('feature'); + $table->timestamp('published_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('announcements'); + } +}; diff --git a/website/database/seeders/AdminUserSeeder.php b/website/database/seeders/AdminUserSeeder.php new file mode 100644 index 0000000..4cd290a --- /dev/null +++ b/website/database/seeders/AdminUserSeeder.php @@ -0,0 +1,51 @@ + 'admin@ezscale.cloud'], + [ + 'name' => 'Admin User', + 'password' => Hash::make('admin123!'), + 'status' => 'active', + 'email_verified_at' => now(), + ] + ); + + if (! $admin->hasRole('admin')) { + $admin->assignRole('admin'); + } + + if (! $admin->profile) { + $admin->profile()->create([]); + } + + $customer = User::firstOrCreate( + ['email' => 'user@ezscale.dev'], + [ + 'name' => 'Test Customer', + 'password' => Hash::make('user123!'), + 'status' => 'active', + 'email_verified_at' => now(), + ] + ); + + if (! $customer->hasRole('customer')) { + $customer->assignRole('customer'); + } + + if (! $customer->profile) { + $customer->profile()->create([]); + } + } +} diff --git a/website/database/seeders/DatabaseSeeder.php b/website/database/seeders/DatabaseSeeder.php index c69d0ff..7e828dd 100644 --- a/website/database/seeders/DatabaseSeeder.php +++ b/website/database/seeders/DatabaseSeeder.php @@ -1,25 +1,19 @@ create(); - - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + $this->call([ + RoleAndPermissionSeeder::class, + PlanSeeder::class, + AdminUserSeeder::class, ]); } } diff --git a/website/database/seeders/PlanSeeder.php b/website/database/seeders/PlanSeeder.php new file mode 100644 index 0000000..4ebea0c --- /dev/null +++ b/website/database/seeders/PlanSeeder.php @@ -0,0 +1,89 @@ + 'VPS Starter', + 'slug' => 'vps-starter', + 'description' => 'Perfect for small projects and development.', + 'service_type' => 'vps', + 'price' => 5.99, + 'billing_cycle' => 'monthly', + 'features' => ['cpu' => '1 vCPU', 'ram' => '1GB', 'disk' => '25GB SSD', 'bandwidth' => '1TB'], + 'sort_order' => 1, + ], + [ + 'name' => 'VPS Pro', + 'slug' => 'vps-pro', + 'description' => 'Ideal for growing applications and websites.', + 'service_type' => 'vps', + 'price' => 19.99, + 'billing_cycle' => 'monthly', + 'features' => ['cpu' => '2 vCPU', 'ram' => '4GB', 'disk' => '80GB SSD', 'bandwidth' => '3TB'], + 'sort_order' => 2, + ], + [ + 'name' => 'VPS Enterprise', + 'slug' => 'vps-enterprise', + 'description' => 'High-performance VPS for demanding workloads.', + 'service_type' => 'vps', + 'price' => 49.99, + 'billing_cycle' => 'monthly', + 'features' => ['cpu' => '4 vCPU', 'ram' => '16GB', 'disk' => '200GB SSD', 'bandwidth' => '10TB'], + 'sort_order' => 3, + ], + // Dedicated Server Plans + [ + 'name' => 'Dedicated Starter', + 'slug' => 'dedicated-starter', + 'description' => 'Entry-level dedicated server.', + 'service_type' => 'dedicated', + 'price' => 99.99, + 'billing_cycle' => 'monthly', + 'features' => ['cpu' => 'Intel Xeon E-2236', 'ram' => '32GB DDR4', 'disk' => '2x 500GB SSD', 'bandwidth' => '10TB'], + 'stock_quantity' => 5, + 'sort_order' => 10, + ], + // Web Hosting Plans + [ + 'name' => 'Hosting Basic', + 'slug' => 'hosting-basic', + 'description' => 'Shared hosting for small websites.', + 'service_type' => 'hosting', + 'price' => 3.99, + 'billing_cycle' => 'monthly', + 'features' => ['disk' => '10GB SSD', 'bandwidth' => '100GB', 'domains' => '1', 'email' => '5 accounts'], + 'sort_order' => 20, + ], + // Game Server Plans + [ + 'name' => 'Minecraft Standard', + 'slug' => 'minecraft-standard', + 'description' => 'Minecraft server for up to 20 players.', + 'service_type' => 'game_server', + 'price' => 9.99, + 'billing_cycle' => 'monthly', + 'features' => ['cpu' => '2 vCPU', 'ram' => '4GB', 'disk' => '30GB SSD', 'players' => '20'], + 'sort_order' => 30, + ], + ]; + + foreach ($plans as $plan) { + Plan::create(array_merge([ + 'currency' => 'USD', + 'status' => 'active', + ], $plan)); + } + } +} diff --git a/website/database/seeders/RoleAndPermissionSeeder.php b/website/database/seeders/RoleAndPermissionSeeder.php new file mode 100644 index 0000000..764754e --- /dev/null +++ b/website/database/seeders/RoleAndPermissionSeeder.php @@ -0,0 +1,36 @@ +forgetCachedPermissions(); + + $permissions = [ + 'manage users', + 'manage services', + 'manage plans', + 'manage invoices', + 'manage coupons', + 'view audit logs', + 'impersonate users', + ]; + + foreach ($permissions as $permission) { + Permission::create(['name' => $permission]); + } + + $adminRole = Role::create(['name' => 'admin']); + $adminRole->givePermissionTo(Permission::all()); + + Role::create(['name' => 'customer']); + } +} diff --git a/website/package-lock.json b/website/package-lock.json index 9d63f34..1540030 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -1,9 +1,14 @@ { - "name": "ezscale", + "name": "website", "lockfileVersion": 3, "requires": true, "packages": { "": { + "dependencies": { + "@inertiajs/vue3": "^2.3.13", + "@vitejs/plugin-vue": "^6.0.4", + "vue": "^3.5.27" + }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", @@ -13,6 +18,52 @@ "vite": "^7.0.7" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -20,7 +71,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -37,7 +87,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -54,7 +103,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -71,7 +119,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -88,7 +135,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -105,7 +151,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -122,7 +167,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -139,7 +183,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -156,7 +199,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -173,7 +215,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -190,7 +231,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -207,7 +247,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -224,7 +263,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -241,7 +279,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -258,7 +295,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -275,7 +311,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -292,7 +327,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -309,7 +343,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -326,7 +359,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -343,7 +375,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -360,7 +391,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -377,7 +407,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -394,7 +423,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -411,7 +439,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -428,7 +455,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -445,7 +471,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -455,6 +480,34 @@ "node": ">=18" } }, + "node_modules/@inertiajs/core": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.13.tgz", + "integrity": "sha512-qMHRnb59k/HehXw/WfQt5kPV0k9RapfFcWJZINJnYMwfHDEJ21iNVZjsJHmDN7yWdZmG1Dxi9FP4xarWWgdosQ==", + "license": "MIT", + "dependencies": { + "@types/lodash-es": "^4.17.12", + "axios": "^1.13.2", + "laravel-precognition": "^1.0.1", + "lodash-es": "^4.17.23", + "qs": "^6.14.1" + } + }, + "node_modules/@inertiajs/vue3": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@inertiajs/vue3/-/vue3-2.3.13.tgz", + "integrity": "sha512-e3uAc4Em6GrUo/51IXmckXyv7GTX0QacTGyP80NVXYhcgUJOH/lp+fzLbAhaiTSoG+4zuT/aKMAJRmXXX6CEGg==", + "license": "MIT", + "dependencies": { + "@inertiajs/core": "2.3.13", + "@types/lodash-es": "^4.17.12", + "laravel-precognition": "^1.0.1", + "lodash-es": "^4.17.23" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -491,7 +544,6 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -505,6 +557,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -512,7 +570,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -526,7 +583,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -540,7 +596,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -554,7 +609,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -568,7 +622,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -582,7 +635,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -596,7 +648,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -610,7 +661,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -624,7 +674,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -638,7 +687,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -652,7 +700,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -666,7 +713,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -680,7 +726,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -694,7 +739,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -708,7 +752,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -722,7 +765,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -736,7 +778,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -750,7 +791,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -764,7 +804,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -778,7 +817,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -792,7 +830,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -806,7 +843,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -820,7 +856,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -834,7 +869,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -848,7 +882,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1131,7 +1164,137 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", "license": "MIT" }, "node_modules/ansi-regex": { @@ -1164,14 +1327,12 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/axios": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -1183,7 +1344,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1193,6 +1353,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1262,7 +1438,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -1296,11 +1471,16 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -1310,7 +1490,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1320,7 +1500,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -1352,11 +1531,22 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1366,7 +1556,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1376,7 +1565,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1389,7 +1577,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1405,7 +1592,6 @@ "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1453,11 +1639,16 @@ "node": ">=6" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1475,7 +1666,6 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, "funding": [ { "type": "individual", @@ -1496,7 +1686,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -1513,7 +1702,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1528,7 +1716,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1548,7 +1735,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1573,7 +1759,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -1587,7 +1772,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1617,7 +1801,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1630,7 +1813,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -1646,7 +1828,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1669,12 +1850,22 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/laravel-precognition": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-1.0.2.tgz", + "integrity": "sha512-0H08JDdMWONrL/N314fvsO3FATJwGGlFKGkMF3nNmizVFJaWs17816iM+sX7Rp8d5hUjYCx6WLfsehSKfaTxjg==", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "lodash-es": "^4.17.21" + } + }, "node_modules/laravel-vite-plugin": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", @@ -1699,7 +1890,7 @@ "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "dev": true, + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -1732,7 +1923,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1753,7 +1943,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1774,7 +1963,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1795,7 +1983,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1816,7 +2003,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1837,7 +2023,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1858,7 +2043,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1879,7 +2063,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1900,7 +2083,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1921,7 +2103,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1942,7 +2123,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1956,11 +2136,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -1970,7 +2155,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1980,7 +2164,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -1990,7 +2173,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -2003,7 +2185,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2018,18 +2199,28 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -2043,7 +2234,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2072,9 +2262,23 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, "license": "MIT" }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2089,7 +2293,6 @@ "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -2153,11 +2356,82 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2232,7 +2506,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2266,7 +2539,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -2362,6 +2634,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/website/package.json b/website/package.json index c22146a..c8a8b29 100644 --- a/website/package.json +++ b/website/package.json @@ -13,5 +13,10 @@ "laravel-vite-plugin": "^2.0.0", "tailwindcss": "^4.0.0", "vite": "^7.0.7" + }, + "dependencies": { + "@inertiajs/vue3": "^2.3.13", + "@vitejs/plugin-vue": "^6.0.4", + "vue": "^3.5.27" } } diff --git a/website/phpunit.xml b/website/phpunit.xml index 74c9a87..cbd3a33 100644 --- a/website/phpunit.xml +++ b/website/phpunit.xml @@ -23,8 +23,8 @@ - - + + diff --git a/website/resources/css/app.css b/website/resources/css/app.css index 6e9f9dd..711a1dd 100644 --- a/website/resources/css/app.css +++ b/website/resources/css/app.css @@ -4,8 +4,9 @@ @source '../../storage/framework/views/*.php'; @source '../**/*.blade.php'; @source '../**/*.js'; +@source '../**/*.vue'; @theme { - --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } diff --git a/website/resources/js/Components/Button.vue b/website/resources/js/Components/Button.vue new file mode 100644 index 0000000..2ec98a4 --- /dev/null +++ b/website/resources/js/Components/Button.vue @@ -0,0 +1,28 @@ + + + diff --git a/website/resources/js/Components/Card.vue b/website/resources/js/Components/Card.vue new file mode 100644 index 0000000..443e0f3 --- /dev/null +++ b/website/resources/js/Components/Card.vue @@ -0,0 +1,12 @@ + + + diff --git a/website/resources/js/Components/FlashMessages.vue b/website/resources/js/Components/FlashMessages.vue new file mode 100644 index 0000000..86e94a9 --- /dev/null +++ b/website/resources/js/Components/FlashMessages.vue @@ -0,0 +1,16 @@ + + + diff --git a/website/resources/js/Components/NavLink.vue b/website/resources/js/Components/NavLink.vue new file mode 100644 index 0000000..d141d6b --- /dev/null +++ b/website/resources/js/Components/NavLink.vue @@ -0,0 +1,22 @@ + + + diff --git a/website/resources/js/Layouts/AdminLayout.vue b/website/resources/js/Layouts/AdminLayout.vue new file mode 100644 index 0000000..df08490 --- /dev/null +++ b/website/resources/js/Layouts/AdminLayout.vue @@ -0,0 +1,46 @@ + + + diff --git a/website/resources/js/Layouts/AppLayout.vue b/website/resources/js/Layouts/AppLayout.vue new file mode 100644 index 0000000..149916e --- /dev/null +++ b/website/resources/js/Layouts/AppLayout.vue @@ -0,0 +1,46 @@ + + + diff --git a/website/resources/js/Layouts/AuthLayout.vue b/website/resources/js/Layouts/AuthLayout.vue new file mode 100644 index 0000000..2ea5ad8 --- /dev/null +++ b/website/resources/js/Layouts/AuthLayout.vue @@ -0,0 +1,16 @@ + + + diff --git a/website/resources/js/Pages/Admin/Dashboard.vue b/website/resources/js/Pages/Admin/Dashboard.vue new file mode 100644 index 0000000..389ae68 --- /dev/null +++ b/website/resources/js/Pages/Admin/Dashboard.vue @@ -0,0 +1,34 @@ + + + diff --git a/website/resources/js/Pages/Auth/ConfirmPassword.vue b/website/resources/js/Pages/Auth/ConfirmPassword.vue new file mode 100644 index 0000000..43b614b --- /dev/null +++ b/website/resources/js/Pages/Auth/ConfirmPassword.vue @@ -0,0 +1,44 @@ + + + diff --git a/website/resources/js/Pages/Auth/ForgotPassword.vue b/website/resources/js/Pages/Auth/ForgotPassword.vue new file mode 100644 index 0000000..e47e2df --- /dev/null +++ b/website/resources/js/Pages/Auth/ForgotPassword.vue @@ -0,0 +1,52 @@ + + + diff --git a/website/resources/js/Pages/Auth/Login.vue b/website/resources/js/Pages/Auth/Login.vue new file mode 100644 index 0000000..f3c4e38 --- /dev/null +++ b/website/resources/js/Pages/Auth/Login.vue @@ -0,0 +1,69 @@ + + + diff --git a/website/resources/js/Pages/Auth/Register.vue b/website/resources/js/Pages/Auth/Register.vue new file mode 100644 index 0000000..d2dd5e0 --- /dev/null +++ b/website/resources/js/Pages/Auth/Register.vue @@ -0,0 +1,85 @@ + + + diff --git a/website/resources/js/Pages/Auth/ResetPassword.vue b/website/resources/js/Pages/Auth/ResetPassword.vue new file mode 100644 index 0000000..6b2a26c --- /dev/null +++ b/website/resources/js/Pages/Auth/ResetPassword.vue @@ -0,0 +1,73 @@ + + + diff --git a/website/resources/js/Pages/Auth/TwoFactorChallenge.vue b/website/resources/js/Pages/Auth/TwoFactorChallenge.vue new file mode 100644 index 0000000..365d3b2 --- /dev/null +++ b/website/resources/js/Pages/Auth/TwoFactorChallenge.vue @@ -0,0 +1,81 @@ + + + diff --git a/website/resources/js/Pages/Auth/VerifyEmail.vue b/website/resources/js/Pages/Auth/VerifyEmail.vue new file mode 100644 index 0000000..3976e98 --- /dev/null +++ b/website/resources/js/Pages/Auth/VerifyEmail.vue @@ -0,0 +1,37 @@ + + + diff --git a/website/resources/js/Pages/Dashboard.vue b/website/resources/js/Pages/Dashboard.vue new file mode 100644 index 0000000..452397e --- /dev/null +++ b/website/resources/js/Pages/Dashboard.vue @@ -0,0 +1,35 @@ + + + diff --git a/website/resources/js/Pages/Marketing/Home.vue b/website/resources/js/Pages/Marketing/Home.vue new file mode 100644 index 0000000..7227c22 --- /dev/null +++ b/website/resources/js/Pages/Marketing/Home.vue @@ -0,0 +1,35 @@ + + + diff --git a/website/resources/js/Pages/Profile/Show.vue b/website/resources/js/Pages/Profile/Show.vue new file mode 100644 index 0000000..7d15253 --- /dev/null +++ b/website/resources/js/Pages/Profile/Show.vue @@ -0,0 +1,81 @@ + + + diff --git a/website/resources/js/Pages/Profile/TwoFactorSetup.vue b/website/resources/js/Pages/Profile/TwoFactorSetup.vue new file mode 100644 index 0000000..8cd056a --- /dev/null +++ b/website/resources/js/Pages/Profile/TwoFactorSetup.vue @@ -0,0 +1,134 @@ + + + diff --git a/website/resources/js/app.js b/website/resources/js/app.js index 1612310..b2bd6d5 100644 --- a/website/resources/js/app.js +++ b/website/resources/js/app.js @@ -1 +1,24 @@ import './bootstrap'; +import '../css/app.css'; + +import { createApp, h } from 'vue'; +import { createInertiaApp } from '@inertiajs/vue3'; +import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; + +const appName = import.meta.env.VITE_APP_NAME || 'EZSCALE'; + +createInertiaApp({ + title: (title) => title ? `${title} - ${appName}` : appName, + resolve: (name) => resolvePageComponent( + `./Pages/${name}.vue`, + import.meta.glob('./Pages/**/*.vue'), + ), + setup({ el, App, props, plugin }) { + return createApp({ render: () => h(App, props) }) + .use(plugin) + .mount(el); + }, + progress: { + color: '#4B5563', + }, +}); diff --git a/website/resources/views/app.blade.php b/website/resources/views/app.blade.php new file mode 100644 index 0000000..2424628 --- /dev/null +++ b/website/resources/views/app.blade.php @@ -0,0 +1,18 @@ + + + + + + + {{ config('app.name', 'EZSCALE') }} + + + + + @vite(['resources/js/app.js']) + @inertiaHead + + + @inertia + + diff --git a/website/routes/account.php b/website/routes/account.php new file mode 100644 index 0000000..87d54e4 --- /dev/null +++ b/website/routes/account.php @@ -0,0 +1,12 @@ +name('account.dashboard'); + +Route::get('/profile', [ProfileController::class, 'show'])->name('account.profile'); +Route::put('/profile', [ProfileController::class, 'update'])->name('account.profile.update'); diff --git a/website/routes/admin.php b/website/routes/admin.php new file mode 100644 index 0000000..b057b9b --- /dev/null +++ b/website/routes/admin.php @@ -0,0 +1,8 @@ +name('admin.dashboard'); diff --git a/website/routes/api.php b/website/routes/api.php new file mode 100644 index 0000000..002f945 --- /dev/null +++ b/website/routes/api.php @@ -0,0 +1,9 @@ +group(function (): void { + // +}); diff --git a/website/routes/marketing.php b/website/routes/marketing.php new file mode 100644 index 0000000..554b4a8 --- /dev/null +++ b/website/routes/marketing.php @@ -0,0 +1,8 @@ + Inertia::render('Marketing/Home'))->name('home'); diff --git a/website/routes/web.php b/website/routes/web.php index fb3bfe2..eeb63f9 100644 --- a/website/routes/web.php +++ b/website/routes/web.php @@ -1,7 +1,9 @@ seed(RoleAndPermissionSeeder::class); + $this->accountUrl = 'http://'.config('app.domains.account'); +}); + +it('renders the login page', function (): void { + $this->get($this->accountUrl.'/login') + ->assertOk(); +}); + +it('authenticates a user with valid credentials', function (): void { + $user = User::factory()->create(); + + $this->post($this->accountUrl.'/login', [ + 'email' => $user->email, + 'password' => 'password', + ])->assertRedirect(); + + $this->assertAuthenticated(); +}); + +it('rejects invalid credentials', function (): void { + $user = User::factory()->create(); + + $this->post($this->accountUrl.'/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); +}); + +it('logs a user out', function (): void { + $user = User::factory()->create(); + + $this->actingAs($user) + ->post($this->accountUrl.'/logout') + ->assertRedirect(); + + $this->assertGuest(); +}); diff --git a/website/tests/Feature/Auth/RegistrationTest.php b/website/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 0000000..c491afa --- /dev/null +++ b/website/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,73 @@ +seed(RoleAndPermissionSeeder::class); + $this->accountUrl = 'http://'.config('app.domains.account'); +}); + +it('renders the registration page', function (): void { + $this->get($this->accountUrl.'/register') + ->assertOk(); +}); + +it('registers a new user', function (): void { + $this->post($this->accountUrl.'/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123!', + 'password_confirmation' => 'password123!', + ])->assertRedirect(); + + $this->assertDatabaseHas('users', [ + 'email' => 'test@example.com', + ]); +}); + +it('assigns customer role on registration', function (): void { + $this->post($this->accountUrl.'/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123!', + 'password_confirmation' => 'password123!', + ]); + + $user = User::where('email', 'test@example.com')->first(); + expect($user->hasRole('customer'))->toBeTrue(); +}); + +it('creates user profile on registration', function (): void { + $this->post($this->accountUrl.'/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123!', + 'password_confirmation' => 'password123!', + ]); + + $user = User::where('email', 'test@example.com')->first(); + expect($user->profile)->not->toBeNull(); +}); + +it('validates registration input', function (): void { + $this->post($this->accountUrl.'/register', [ + 'name' => '', + 'email' => 'not-an-email', + 'password' => 'short', + 'password_confirmation' => 'mismatch', + ])->assertSessionHasErrors(['name', 'email', 'password']); +}); + +it('prevents duplicate email registration', function (): void { + User::factory()->create(['email' => 'taken@example.com']); + + $this->post($this->accountUrl.'/register', [ + 'name' => 'Test User', + 'email' => 'taken@example.com', + 'password' => 'password123!', + 'password_confirmation' => 'password123!', + ])->assertSessionHasErrors('email'); +}); diff --git a/website/tests/Feature/Auth/RoleBasedAccessTest.php b/website/tests/Feature/Auth/RoleBasedAccessTest.php new file mode 100644 index 0000000..c454d47 --- /dev/null +++ b/website/tests/Feature/Auth/RoleBasedAccessTest.php @@ -0,0 +1,44 @@ +seed(RoleAndPermissionSeeder::class); + $this->accountUrl = 'http://'.config('app.domains.account'); +}); + +it('redirects unauthenticated users to login', function (): void { + $this->get($this->accountUrl.'/dashboard') + ->assertRedirect($this->accountUrl.'/login'); +}); + +it('identifies admin users correctly', function (): void { + $admin = User::factory()->admin()->create(); + + expect($admin->isAdmin())->toBeTrue(); + expect($admin->isCustomer())->toBeFalse(); +}); + +it('identifies customer users correctly', function (): void { + $customer = User::factory()->customer()->create(); + + expect($customer->isCustomer())->toBeTrue(); + expect($customer->isAdmin())->toBeFalse(); +}); + +it('identifies suspended users correctly', function (): void { + $user = User::factory()->suspended()->create(); + + expect($user->isSuspended())->toBeTrue(); + expect($user->isBanned())->toBeFalse(); +}); + +it('identifies banned users correctly', function (): void { + $user = User::factory()->banned()->create(); + + expect($user->isBanned())->toBeTrue(); + expect($user->isSuspended())->toBeFalse(); +}); diff --git a/website/tests/Feature/ExampleTest.php b/website/tests/Feature/ExampleTest.php index 16fe53c..d59ff2e 100644 --- a/website/tests/Feature/ExampleTest.php +++ b/website/tests/Feature/ExampleTest.php @@ -1,7 +1,8 @@ get('/'); +declare(strict_types=1); - $response->assertStatus(200); +test('the application redirects to account login', function () { + $this->get('/') + ->assertRedirect('https://'.config('app.domains.account').'/login'); }); diff --git a/website/tests/Feature/Models/PlanTest.php b/website/tests/Feature/Models/PlanTest.php new file mode 100644 index 0000000..ee748e8 --- /dev/null +++ b/website/tests/Feature/Models/PlanTest.php @@ -0,0 +1,32 @@ +create(); + + expect($plan)->toBeInstanceOf(Plan::class); + expect($plan->status)->toBe('active'); +}); + +it('casts features to array', function (): void { + $plan = Plan::factory()->create([ + 'features' => ['cpu' => '2 vCPU', 'ram' => '4GB'], + ]); + + expect($plan->features)->toBeArray(); + expect($plan->features['cpu'])->toBe('2 vCPU'); +}); + +it('checks availability correctly', function (): void { + $plan = Plan::factory()->create(['status' => 'active', 'stock_quantity' => null]); + expect($plan->isAvailable())->toBeTrue(); + + $plan = Plan::factory()->create(['status' => 'hidden']); + expect($plan->isAvailable())->toBeFalse(); + + $plan = Plan::factory()->create(['status' => 'active', 'stock_quantity' => 0]); + expect($plan->isAvailable())->toBeFalse(); +}); diff --git a/website/tests/Feature/Models/UserTest.php b/website/tests/Feature/Models/UserTest.php new file mode 100644 index 0000000..9cfb110 --- /dev/null +++ b/website/tests/Feature/Models/UserTest.php @@ -0,0 +1,42 @@ +seed(RoleAndPermissionSeeder::class); +}); + +it('has correct fillable attributes', function (): void { + $user = new User; + + expect($user->getFillable())->toBe([ + 'name', 'email', 'password', 'status', 'phone', 'company', + ]); +}); + +it('has correct hidden attributes', function (): void { + $user = new User; + + expect($user->getHidden())->toBe([ + 'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', + ]); +}); + +it('has profile relationship', function (): void { + $user = User::factory()->create(); + $profile = UserProfile::factory()->create(['user_id' => $user->id]); + + expect($user->profile)->toBeInstanceOf(UserProfile::class); + expect($user->profile->id)->toBe($profile->id); +}); + +it('creates user with factory', function (): void { + $user = User::factory()->create(); + + expect($user)->toBeInstanceOf(User::class); + expect($user->status)->toBe('active'); +}); diff --git a/website/tests/Pest.php b/website/tests/Pest.php index 6371a72..81acb27 100644 --- a/website/tests/Pest.php +++ b/website/tests/Pest.php @@ -1,47 +1,5 @@ extend(Tests\TestCase::class) - // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); - -/* -|-------------------------------------------------------------------------- -| Expectations -|-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| -*/ - -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - -/* -|-------------------------------------------------------------------------- -| Functions -|-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| -*/ - -function something() -{ - // .. -} diff --git a/website/vite.config.js b/website/vite.config.js index e5503b4..c7891dc 100644 --- a/website/vite.config.js +++ b/website/vite.config.js @@ -1,14 +1,23 @@ import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import tailwindcss from '@tailwindcss/vite'; +import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], + input: ['resources/js/app.js'], refresh: true, }), tailwindcss(), + vue({ + template: { + transformAssetUrls: { + base: null, + includeAbsolute: false, + }, + }, + }), ], server: { watch: {