Implement Phase 1: Foundation & Core Setup
Complete foundation for the EZSCALE billing platform replacing WHMCS: - Install Composer deps (Fortify, Passport, Cashier, PayPal, Spatie Permissions, Inertia) - Install Vue 3 + Inertia.js with Vite, 3 layouts (App, Auth, Admin) - Configure subdomain routing (marketing, account, admin) with domain-based route files - Create 30 database migrations (15 custom tables + package defaults) - Create 14 Eloquent models with relationships, factories, and encrypted casts - Set up Fortify auth with 7 Vue pages (Login, Register, ForgotPassword, ResetPassword, VerifyEmail, ConfirmPassword, TwoFactorChallenge) - Add 2FA TOTP setup page with QR code and recovery codes - Configure middleware (Inertia, Spatie roles/permissions, EnsureUserNotSuspended) - Create seeders for roles/permissions, sample plans, and admin user - Build dashboard controllers and Vue pages for customer and admin panels - Add 4 shared Vue components (Card, Button, NavLink, FlashMessages) - Generate Passport OAuth2 keys for future SSO/API use - Write 24 Pest tests (auth, role-based access, models) — all passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
124
website/.claude/skills/tailwindcss-development/SKILL.md
Normal file
124
website/.claude/skills/tailwindcss-development/SKILL.md
Normal file
@@ -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:
|
||||
|
||||
<code-snippet name="CSS-First Config" lang="css">
|
||||
@theme {
|
||||
--color-brand: oklch(0.72 0.11 178);
|
||||
}
|
||||
</code-snippet>
|
||||
|
||||
### Import Syntax
|
||||
|
||||
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||
|
||||
<code-snippet name="v4 Import Syntax" lang="diff">
|
||||
- @tailwind base;
|
||||
- @tailwind components;
|
||||
- @tailwind utilities;
|
||||
+ @import "tailwindcss";
|
||||
</code-snippet>
|
||||
|
||||
### 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:
|
||||
|
||||
<code-snippet name="Gap Utilities" lang="html">
|
||||
<div class="flex gap-8">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## 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:
|
||||
|
||||
<code-snippet name="Dark Mode" lang="html">
|
||||
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
Content adapts to color scheme
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flexbox Layout
|
||||
|
||||
<code-snippet name="Flexbox Layout" lang="html">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>Left content</div>
|
||||
<div>Right content</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
### Grid Layout
|
||||
|
||||
<code-snippet name="Grid Layout" lang="html">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div>Card 1</div>
|
||||
<div>Card 2</div>
|
||||
<div>Card 3</div>
|
||||
</div>
|
||||
</code-snippet>
|
||||
|
||||
## 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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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.
|
||||
</laravel-boost-guidelines>
|
||||
|
||||
46
website/app/Actions/Fortify/CreateNewUser.php
Normal file
46
website/app/Actions/Fortify/CreateNewUser.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/** @param array<string, string> $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;
|
||||
});
|
||||
}
|
||||
}
|
||||
18
website/app/Actions/Fortify/PasswordValidationRules.php
Normal file
18
website/app/Actions/Fortify/PasswordValidationRules.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
trait PasswordValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate passwords.
|
||||
*
|
||||
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||
*/
|
||||
protected function passwordRules(): array
|
||||
{
|
||||
return ['required', 'string', Password::default(), 'confirmed'];
|
||||
}
|
||||
}
|
||||
29
website/app/Actions/Fortify/ResetUserPassword.php
Normal file
29
website/app/Actions/Fortify/ResetUserPassword.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
32
website/app/Actions/Fortify/UpdateUserPassword.php
Normal file
32
website/app/Actions/Fortify/UpdateUserPassword.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param array<string, string> $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();
|
||||
}
|
||||
}
|
||||
58
website/app/Actions/Fortify/UpdateUserProfileInformation.php
Normal file
58
website/app/Actions/Fortify/UpdateUserProfileInformation.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param array<string, string> $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<string, string> $input
|
||||
*/
|
||||
protected function updateVerifiedUser(User $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
'name' => $input['name'],
|
||||
'email' => $input['email'],
|
||||
'email_verified_at' => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
||||
23
website/app/Http/Controllers/Account/DashboardController.php
Normal file
23
website/app/Http/Controllers/Account/DashboardController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'servicesCount' => $user->services()->count(),
|
||||
'activeServicesCount' => $user->services()->where('status', 'active')->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
website/app/Http/Controllers/Account/ProfileController.php
Normal file
33
website/app/Http/Controllers/Account/ProfileController.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Account;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request): Response
|
||||
{
|
||||
return Inertia::render('Profile/Show', [
|
||||
'user' => $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();
|
||||
}
|
||||
}
|
||||
23
website/app/Http/Controllers/Admin/DashboardController.php
Normal file
23
website/app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('Admin/Dashboard', [
|
||||
'totalCustomers' => User::role('customer')->count(),
|
||||
'totalServices' => Service::count(),
|
||||
'activeServices' => Service::where('status', 'active')->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
website/app/Http/Middleware/EnsureUserNotSuspended.php
Normal file
21
website/app/Http/Middleware/EnsureUserNotSuspended.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureUserNotSuspended
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->user()?->isSuspended()) {
|
||||
abort(403, 'Your account has been suspended.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
27
website/app/Http/Responses/LoginResponse.php
Normal file
27
website/app/Http/Responses/LoginResponse.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Responses;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LoginResponse implements LoginResponseContract
|
||||
{
|
||||
public function toResponse($request): Response
|
||||
{
|
||||
/** @var Request $request */
|
||||
$user = $request->user();
|
||||
|
||||
if ($user && $user->hasRole('admin')) {
|
||||
$url = 'https://'.config('app.domains.admin').'/dashboard';
|
||||
|
||||
return Inertia::location($url);
|
||||
}
|
||||
|
||||
return redirect()->intended('/dashboard');
|
||||
}
|
||||
}
|
||||
39
website/app/Models/Announcement.php
Normal file
39
website/app/Models/Announcement.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Announcement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'content',
|
||||
'type',
|
||||
'published_at',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'published_at' => '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();
|
||||
}
|
||||
}
|
||||
43
website/app/Models/AuditLog.php
Normal file
43
website/app/Models/AuditLog.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AuditLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'admin_id',
|
||||
'action',
|
||||
'resource_type',
|
||||
'resource_id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'changes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'changes' => 'array',
|
||||
'resource_id' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
48
website/app/Models/BandwidthUsage.php
Normal file
48
website/app/Models/BandwidthUsage.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BandwidthUsage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'bandwidth_usage';
|
||||
|
||||
protected $fillable = [
|
||||
'service_id',
|
||||
'period_start',
|
||||
'period_end',
|
||||
'bytes_in',
|
||||
'bytes_out',
|
||||
'total_bytes',
|
||||
'quota_bytes',
|
||||
'overage_bytes',
|
||||
'overage_charge',
|
||||
'source',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'period_start' => '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);
|
||||
}
|
||||
}
|
||||
54
website/app/Models/Coupon.php
Normal file
54
website/app/Models/Coupon.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Coupon extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'type',
|
||||
'value',
|
||||
'currency',
|
||||
'applies_to',
|
||||
'max_uses',
|
||||
'times_used',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'value' => '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;
|
||||
}
|
||||
}
|
||||
44
website/app/Models/CouponRedemption.php
Normal file
44
website/app/Models/CouponRedemption.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Cashier\Subscription;
|
||||
|
||||
class CouponRedemption extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'coupon_id',
|
||||
'user_id',
|
||||
'subscription_id',
|
||||
'discount_amount',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'discount_amount' => '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);
|
||||
}
|
||||
}
|
||||
61
website/app/Models/Invoice.php
Normal file
61
website/app/Models/Invoice.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Laravel\Cashier\Subscription;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'subscription_id',
|
||||
'gateway',
|
||||
'gateway_invoice_id',
|
||||
'number',
|
||||
'total',
|
||||
'tax',
|
||||
'currency',
|
||||
'status',
|
||||
'invoice_pdf',
|
||||
'due_date',
|
||||
'paid_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'total' => '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);
|
||||
}
|
||||
}
|
||||
34
website/app/Models/InvoiceItem.php
Normal file
34
website/app/Models/InvoiceItem.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class InvoiceItem extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'invoice_id',
|
||||
'description',
|
||||
'amount',
|
||||
'quantity',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'amount' => 'decimal:2',
|
||||
'quantity' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function invoice(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
}
|
||||
52
website/app/Models/PaymentTransaction.php
Normal file
52
website/app/Models/PaymentTransaction.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Cashier\Subscription;
|
||||
|
||||
class PaymentTransaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'subscription_id',
|
||||
'invoice_id',
|
||||
'gateway',
|
||||
'gateway_transaction_id',
|
||||
'amount',
|
||||
'currency',
|
||||
'status',
|
||||
'payment_method',
|
||||
'description',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'amount' => '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);
|
||||
}
|
||||
}
|
||||
58
website/app/Models/Plan.php
Normal file
58
website/app/Models/Plan.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Plan extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'service_type',
|
||||
'price',
|
||||
'currency',
|
||||
'billing_cycle',
|
||||
'stripe_price_id',
|
||||
'paypal_plan_id',
|
||||
'features',
|
||||
'stock_quantity',
|
||||
'status',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'price' => '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;
|
||||
}
|
||||
}
|
||||
47
website/app/Models/ProvisioningLog.php
Normal file
47
website/app/Models/ProvisioningLog.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ProvisioningLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'service_id',
|
||||
'user_id',
|
||||
'action',
|
||||
'platform',
|
||||
'platform_response',
|
||||
'status',
|
||||
'error_message',
|
||||
'admin_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'platform_response' => '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');
|
||||
}
|
||||
}
|
||||
76
website/app/Models/Service.php
Normal file
76
website/app/Models/Service.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Laravel\Cashier\Subscription;
|
||||
|
||||
class Service extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'subscription_id',
|
||||
'plan_id',
|
||||
'service_type',
|
||||
'platform',
|
||||
'platform_service_id',
|
||||
'status',
|
||||
'ipv4_address',
|
||||
'ipv6_address',
|
||||
'hostname',
|
||||
'domain',
|
||||
'credentials',
|
||||
'provisioned_at',
|
||||
'suspended_at',
|
||||
'terminated_at',
|
||||
'auto_renew',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'credentials' => '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';
|
||||
}
|
||||
}
|
||||
36
website/app/Models/SupportTicket.php
Normal file
36
website/app/Models/SupportTicket.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SupportTicket extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'supportpal_ticket_id',
|
||||
'subject',
|
||||
'status',
|
||||
'priority',
|
||||
'last_reply_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'supportpal_ticket_id' => 'integer',
|
||||
'last_reply_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Cashier\Billable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use Billable, HasApiTokens, HasFactory, HasRoles, Notifiable, TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
/** @var list<string> */
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'status',
|
||||
'phone',
|
||||
'company',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
/** @var list<string> */
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
/** @return array<string, string> */
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
47
website/app/Models/UserProfile.php
Normal file
47
website/app/Models/UserProfile.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserProfile extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'billing_address_line1',
|
||||
'billing_address_line2',
|
||||
'billing_city',
|
||||
'billing_state',
|
||||
'billing_zip',
|
||||
'billing_country',
|
||||
'shipping_address_line1',
|
||||
'shipping_address_line2',
|
||||
'shipping_city',
|
||||
'shipping_state',
|
||||
'shipping_zip',
|
||||
'shipping_country',
|
||||
'tax_id',
|
||||
'tax_exempt',
|
||||
'company_name',
|
||||
'company_vat',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'tax_exempt' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\Responses\LoginResponse;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->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());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
54
website/app/Providers/FortifyServiceProvider.php
Normal file
54
website/app/Providers/FortifyServiceProvider.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserProfileInformation;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Fortify;
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
|
||||
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
|
||||
Fortify::loginView(fn () => 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'));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"mcp": true,
|
||||
"sail": false,
|
||||
"skills": [
|
||||
"pest-testing"
|
||||
"pest-testing",
|
||||
"tailwindcss-development"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
//
|
||||
|
||||
0
website/bootstrap/cache/.gitignore
vendored
Normal file → Executable file
0
website/bootstrap/cache/.gitignore
vendored
Normal file → Executable file
@@ -2,4 +2,5 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
1774
website/composer.lock
generated
1774
website/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -40,6 +40,11 @@ return [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'passport',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
127
website/config/cashier.php
Normal file
127
website/config/cashier.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Cashier\Console\WebhookCommand;
|
||||
use Laravel\Cashier\Invoices\DompdfInvoiceRenderer;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stripe Keys
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Stripe publishable key and secret key give you access to Stripe's
|
||||
| API. The "publishable" key is typically used when interacting with
|
||||
| Stripe.js while the "secret" key accesses private API endpoints.
|
||||
|
|
||||
*/
|
||||
|
||||
'key' => 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'),
|
||||
|
||||
];
|
||||
159
website/config/fortify.php
Normal file
159
website/config/fortify.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fortify Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which authentication guard Fortify will use while
|
||||
| authenticating users. This value should correspond with one of your
|
||||
| guards that is already present in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => '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,
|
||||
]),
|
||||
],
|
||||
|
||||
];
|
||||
46
website/config/passport.php
Normal file
46
website/config/passport.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Passport Guard
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which authentication guard Passport will use when
|
||||
| authenticating users. This value should correspond with one of your
|
||||
| guards that is already present in your "auth" configuration file.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => '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'),
|
||||
|
||||
];
|
||||
26
website/config/paypal.php
Normal file
26
website/config/paypal.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PayPal Setting & API Credentials
|
||||
* Created by Raza Mehdi <srmk@outlook.com>.
|
||||
*/
|
||||
|
||||
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.
|
||||
];
|
||||
202
website/config/permission.php
Normal file
202
website/config/permission.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'models' => [
|
||||
|
||||
/*
|
||||
* 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',
|
||||
],
|
||||
];
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
|
||||
22
website/database/factories/AnnouncementFactory.php
Normal file
22
website/database/factories/AnnouncementFactory.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Announcement> */
|
||||
class AnnouncementFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'title' => fake()->sentence(),
|
||||
'content' => fake()->paragraphs(2, true),
|
||||
'type' => fake()->randomElement(['maintenance', 'feature', 'outage']),
|
||||
'published_at' => now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
23
website/database/factories/AuditLogFactory.php
Normal file
23
website/database/factories/AuditLogFactory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AuditLog> */
|
||||
class AuditLogFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
25
website/database/factories/CouponFactory.php
Normal file
25
website/database/factories/CouponFactory.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Coupon> */
|
||||
class CouponFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
35
website/database/factories/InvoiceFactory.php
Normal file
35
website/database/factories/InvoiceFactory.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Invoice> */
|
||||
class InvoiceFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
website/database/factories/PlanFactory.php
Normal file
36
website/database/factories/PlanFactory.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Plan> */
|
||||
class PlanFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
54
website/database/factories/ServiceFactory.php
Normal file
54
website/database/factories/ServiceFactory.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Service> */
|
||||
class ServiceFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
@@ -11,16 +13,9 @@ use Illuminate\Support\Str;
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
/** @return array<string, mixed> */
|
||||
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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
26
website/database/factories/UserProfileFactory.php
Normal file
26
website/database/factories/UserProfileFactory.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\UserProfile> */
|
||||
class UserProfileFactory extends Factory
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('oauth_auth_codes', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('oauth_access_tokens', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('oauth_refresh_tokens', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('oauth_clients', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$teams = config('permission.teams');
|
||||
$tableNames = config('permission.table_names');
|
||||
$columnNames = config('permission.column_names');
|
||||
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||
|
||||
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
// $table->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']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscriptions', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('oauth_device_codes', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscription_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('subscription_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('subscription_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_profiles', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('plans', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('subscriptions', function (Blueprint $table): void {
|
||||
$table->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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoices', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoice_items', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payment_transactions', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('coupons', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('coupon_redemptions', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('services', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('provisioning_logs', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bandwidth_usage', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('audit_logs', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('support_tickets', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('announcements', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
51
website/database/seeders/AdminUserSeeder.php
Normal file
51
website/database/seeders/AdminUserSeeder.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminUserSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$admin = User::firstOrCreate(
|
||||
['email' => '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([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
$this->call([
|
||||
RoleAndPermissionSeeder::class,
|
||||
PlanSeeder::class,
|
||||
AdminUserSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
89
website/database/seeders/PlanSeeder.php
Normal file
89
website/database/seeders/PlanSeeder.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Plan;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PlanSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$plans = [
|
||||
// VPS Plans
|
||||
[
|
||||
'name' => '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));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
website/database/seeders/RoleAndPermissionSeeder.php
Normal file
36
website/database/seeders/RoleAndPermissionSeeder.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class RoleAndPermissionSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
app()[\Spatie\Permission\PermissionRegistrar::class]->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']);
|
||||
}
|
||||
}
|
||||
500
website/package-lock.json
generated
500
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_CONNECTION" value="mysql"/>
|
||||
<env name="DB_DATABASE" value="ezscale_billing_test"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
28
website/resources/js/Components/Button.vue
Normal file
28
website/resources/js/Components/Button.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'submit',
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
disabled: Boolean,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'px-4 py-2 text-sm font-medium rounded-md disabled:opacity-50',
|
||||
variant === 'primary' && 'bg-blue-600 text-white hover:bg-blue-700',
|
||||
variant === 'secondary' && 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50',
|
||||
variant === 'danger' && 'bg-red-600 text-white hover:bg-red-700',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
12
website/resources/js/Components/Card.vue
Normal file
12
website/resources/js/Components/Card.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: String,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
||||
<h3 v-if="title" class="text-sm font-medium text-gray-500 mb-2">{{ title }}</h3>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
16
website/resources/js/Components/FlashMessages.vue
Normal file
16
website/resources/js/Components/FlashMessages.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const page = usePage();
|
||||
const flash = computed(() => page.props.flash || {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="flash.success" class="mb-4 rounded-md bg-green-50 p-4">
|
||||
<p class="text-sm font-medium text-green-800">{{ flash.success }}</p>
|
||||
</div>
|
||||
<div v-if="flash.error" class="mb-4 rounded-md bg-red-50 p-4">
|
||||
<p class="text-sm font-medium text-red-800">{{ flash.error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
22
website/resources/js/Components/NavLink.vue
Normal file
22
website/resources/js/Components/NavLink.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
href: String,
|
||||
active: Boolean,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link
|
||||
:href="href"
|
||||
:class="[
|
||||
'px-3 py-2 rounded-md text-sm font-medium',
|
||||
active
|
||||
? 'bg-gray-100 text-gray-900'
|
||||
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
46
website/resources/js/Layouts/AdminLayout.vue
Normal file
46
website/resources/js/Layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
|
||||
const page = usePage();
|
||||
const user = page.props.auth?.user;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-900">
|
||||
<nav class="bg-gray-800 border-b border-gray-700">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<Link href="/dashboard" class="text-xl font-bold text-white">
|
||||
EZSCALE <span class="text-xs font-normal text-gray-400">Admin</span>
|
||||
</Link>
|
||||
<div class="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span v-if="user" class="text-sm text-gray-300">{{ user.name }}</span>
|
||||
<Link
|
||||
v-if="user"
|
||||
href="/logout"
|
||||
method="post"
|
||||
as="button"
|
||||
class="text-sm text-gray-400 hover:text-white"
|
||||
>
|
||||
Log out
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
46
website/resources/js/Layouts/AppLayout.vue
Normal file
46
website/resources/js/Layouts/AppLayout.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
|
||||
const page = usePage();
|
||||
const user = page.props.auth?.user;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<Link href="/dashboard" class="text-xl font-bold text-gray-900">
|
||||
EZSCALE
|
||||
</Link>
|
||||
<div class="hidden sm:ml-8 sm:flex sm:space-x-4">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span v-if="user" class="text-sm text-gray-600">{{ user.name }}</span>
|
||||
<Link
|
||||
v-if="user"
|
||||
href="/logout"
|
||||
method="post"
|
||||
as="button"
|
||||
class="text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Log out
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
16
website/resources/js/Layouts/AuthLayout.vue
Normal file
16
website/resources/js/Layouts/AuthLayout.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">EZSCALE</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">Cloud Hosting Platform</p>
|
||||
</div>
|
||||
<div class="bg-white shadow-sm rounded-lg border border-gray-200 p-8">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
website/resources/js/Pages/Admin/Dashboard.vue
Normal file
34
website/resources/js/Pages/Admin/Dashboard.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||
|
||||
defineOptions({ layout: AdminLayout });
|
||||
|
||||
defineProps({
|
||||
totalCustomers: Number,
|
||||
totalServices: Number,
|
||||
activeServices: Number,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white mb-6">Admin Dashboard</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||
<h3 class="text-sm font-medium text-gray-400">Total Customers</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-white">{{ totalCustomers }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||
<h3 class="text-sm font-medium text-gray-400">Total Services</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-white">{{ totalServices }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800 rounded-lg border border-gray-700 p-6">
|
||||
<h3 class="text-sm font-medium text-gray-400">Active Services</h3>
|
||||
<p class="mt-2 text-3xl font-bold text-green-400">{{ activeServices }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
44
website/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
44
website/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import AuthLayout from '@/Layouts/AuthLayout.vue';
|
||||
|
||||
defineOptions({ layout: AuthLayout });
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post('/user/confirm-password', {
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Confirm your password</h2>
|
||||
<p class="text-sm text-gray-600 mb-6">Please confirm your password before continuing.</p>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
autofocus
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.password" class="mt-1 text-sm text-red-600">{{ form.errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
52
website/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
52
website/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import AuthLayout from '@/Layouts/AuthLayout.vue';
|
||||
|
||||
defineOptions({ layout: AuthLayout });
|
||||
|
||||
defineProps({
|
||||
status: String,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post('/forgot-password');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Reset your password</h2>
|
||||
<p class="text-sm text-gray-600 mb-6">Enter your email and we'll send you a reset link.</p>
|
||||
|
||||
<div v-if="status" class="mb-4 text-sm font-medium text-green-600">{{ status }}</div>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.email" class="mt-1 text-sm text-red-600">{{ form.errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Send reset link
|
||||
</button>
|
||||
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
<a href="/login" class="text-blue-600 hover:text-blue-500">Back to login</a>
|
||||
</p>
|
||||
</form>
|
||||
</template>
|
||||
69
website/resources/js/Pages/Auth/Login.vue
Normal file
69
website/resources/js/Pages/Auth/Login.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import AuthLayout from '@/Layouts/AuthLayout.vue';
|
||||
|
||||
defineOptions({ layout: AuthLayout });
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post('/login', {
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Sign in to your account</h2>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
autofocus
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.email" class="mt-1 text-sm text-red-600">{{ form.errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.password" class="mt-1 text-sm text-red-600">{{ form.errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<input v-model="form.remember" type="checkbox" class="rounded border-gray-300 text-blue-600" />
|
||||
<span class="ml-2 text-sm text-gray-600">Remember me</span>
|
||||
</label>
|
||||
<a href="/forgot-password" class="text-sm text-blue-600 hover:text-blue-500">Forgot password?</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
Don't have an account? <a href="/register" class="text-blue-600 hover:text-blue-500">Sign up</a>
|
||||
</p>
|
||||
</form>
|
||||
</template>
|
||||
85
website/resources/js/Pages/Auth/Register.vue
Normal file
85
website/resources/js/Pages/Auth/Register.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import AuthLayout from '@/Layouts/AuthLayout.vue';
|
||||
|
||||
defineOptions({ layout: AuthLayout });
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post('/register', {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Create an account</h2>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.name" class="mt-1 text-sm text-red-600">{{ form.errors.name }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.email" class="mt-1 text-sm text-red-600">{{ form.errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.password" class="mt-1 text-sm text-red-600">{{ form.errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password_confirmation" class="block text-sm font-medium text-gray-700">Confirm Password</label>
|
||||
<input
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Create account
|
||||
</button>
|
||||
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
Already have an account? <a href="/login" class="text-blue-600 hover:text-blue-500">Sign in</a>
|
||||
</p>
|
||||
</form>
|
||||
</template>
|
||||
73
website/resources/js/Pages/Auth/ResetPassword.vue
Normal file
73
website/resources/js/Pages/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import AuthLayout from '@/Layouts/AuthLayout.vue';
|
||||
|
||||
defineOptions({ layout: AuthLayout });
|
||||
|
||||
const props = defineProps({
|
||||
token: String,
|
||||
email: String,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
token: props.token,
|
||||
email: props.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post('/reset-password', {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Set new password</h2>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="form.email"
|
||||
type="email"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.email" class="mt-1 text-sm text-red-600">{{ form.errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">New Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.password" class="mt-1 text-sm text-red-600">{{ form.errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password_confirmation" class="block text-sm font-medium text-gray-700">Confirm Password</label>
|
||||
<input
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Reset password
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
81
website/resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
81
website/resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import AuthLayout from '@/Layouts/AuthLayout.vue';
|
||||
|
||||
defineOptions({ layout: AuthLayout });
|
||||
|
||||
const useRecovery = ref(false);
|
||||
|
||||
const form = useForm({
|
||||
code: '',
|
||||
recovery_code: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post('/two-factor-challenge', {
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleRecovery = () => {
|
||||
useRecovery.value = !useRecovery.value;
|
||||
form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Two-Factor Authentication</h2>
|
||||
<p class="text-sm text-gray-600 mb-6">
|
||||
<template v-if="!useRecovery">
|
||||
Enter the authentication code from your authenticator app.
|
||||
</template>
|
||||
<template v-else>
|
||||
Enter one of your emergency recovery codes.
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="submit" class="space-y-4">
|
||||
<div v-if="!useRecovery">
|
||||
<label for="code" class="block text-sm font-medium text-gray-700">Code</label>
|
||||
<input
|
||||
id="code"
|
||||
v-model="form.code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.code" class="mt-1 text-sm text-red-600">{{ form.errors.code }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<label for="recovery_code" class="block text-sm font-medium text-gray-700">Recovery Code</label>
|
||||
<input
|
||||
id="recovery_code"
|
||||
v-model="form.recovery_code"
|
||||
type="text"
|
||||
autofocus
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 border"
|
||||
/>
|
||||
<p v-if="form.errors.recovery_code" class="mt-1 text-sm text-red-600">{{ form.errors.recovery_code }}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="toggleRecovery"
|
||||
class="w-full text-center text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
{{ useRecovery ? 'Use authentication code' : 'Use a recovery code' }}
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user