updates for admin area
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
# Security baseline
|
||||
- Enforce HTTPS/HSTS in production.
|
||||
- Keep private/verification uploads outside public storage; authorize every download.
|
||||
- Validate MIME and image decoding; strip metadata where appropriate.
|
||||
- Use policies for every tenant-owned resource.
|
||||
- Enable 2FA for admin/DJ accounts.
|
||||
- Verify Stripe webhook signatures and make handlers idempotent.
|
||||
- Rate-limit auth, enquiry, review and messaging endpoints.
|
||||
- Audit privileged actions; hash IP addresses if retained.
|
||||
- Never store card details.
|
||||
- Run queues under Supervisor/systemd and keep dependencies patched.
|
||||
- Replace placeholder legal policies before launch.
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Booking;
|
||||
use App\Models\Business;
|
||||
use App\Models\DjProfile;
|
||||
use App\Models\Enquiry;
|
||||
use App\Models\Review;
|
||||
use App\Models\User;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SectionController extends Controller
|
||||
{
|
||||
public function __invoke(string $section): View
|
||||
{
|
||||
$allowed = [
|
||||
'users', 'djs', 'clients', 'bookings', 'payments', 'reviews', 'messages',
|
||||
'notifications', 'support', 'reports', 'content', 'roles', 'privacy', 'settings',
|
||||
];
|
||||
|
||||
abort_unless(in_array($section, $allowed, true), 404);
|
||||
|
||||
$counts = [
|
||||
'users' => User::count(),
|
||||
'djs' => DjProfile::count(),
|
||||
'businesses' => Business::count(),
|
||||
'clients' => User::role('client')->count(),
|
||||
'bookings' => Booking::count(),
|
||||
'enquiries' => Enquiry::count(),
|
||||
'reviews' => Review::count(),
|
||||
];
|
||||
|
||||
return view('admin.section', compact('section', 'counts'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
return back()->withErrors(['email' => 'The provided credentials do not match our records.'])->onlyInput('email');
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('dashboard'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('home');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Password as PasswordRule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', PasswordRule::defaults()],
|
||||
]);
|
||||
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user, string $password): void {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
return $status === Password::PASSWORD_RESET
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PasswordResetLinkController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.forgot-password');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate(['email' => ['required', 'email']]);
|
||||
|
||||
$status = Password::sendResetLink($request->only('email'));
|
||||
|
||||
return $status === Password::RESET_LINK_SENT
|
||||
? back()->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))->withErrors(['email' => __($status)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
]);
|
||||
|
||||
$user = User::create($data);
|
||||
$user->assignRole('client');
|
||||
|
||||
event(new Registered($user));
|
||||
Auth::login($user);
|
||||
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class Booking extends Model
|
||||
{
|
||||
@@ -15,4 +17,24 @@ class Booking extends Model
|
||||
{
|
||||
return ['event_date' => 'date', 'total' => 'decimal:2', 'deposit' => 'decimal:2', 'paid_at' => 'datetime'];
|
||||
}
|
||||
|
||||
public function enquiry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Enquiry::class);
|
||||
}
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'client_id');
|
||||
}
|
||||
|
||||
public function djProfile(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DjProfile::class);
|
||||
}
|
||||
|
||||
public function review(): HasOne
|
||||
{
|
||||
return $this->hasOne(Review::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
class Enquiry extends Model
|
||||
{
|
||||
@@ -15,4 +17,19 @@ class Enquiry extends Model
|
||||
{
|
||||
return ['event_date' => 'date', 'budget' => 'decimal:2'];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function djProfile(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DjProfile::class);
|
||||
}
|
||||
|
||||
public function booking(): HasOne
|
||||
{
|
||||
return $this->hasOne(Booking::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,26 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Review extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
|
||||
protected $fillable = ['booking_id', 'user_id', 'dj_profile_id', 'rating', 'title', 'body', 'status', 'dj_reply'];
|
||||
|
||||
public function booking(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Booking::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function djProfile(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DjProfile::class);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-4
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
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\Sanctum\HasApiTokens;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens,HasRoles,Notifiable;
|
||||
use Billable, HasApiTokens, HasFactory, HasRoles, Notifiable;
|
||||
|
||||
protected $fillable = ['name', 'email', 'password', 'phone', 'avatar_path', 'marketing_opt_in'];
|
||||
|
||||
@@ -17,16 +22,31 @@ class User extends Authenticatable
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['email_verified_at' => 'datetime', 'password' => 'hashed', 'marketing_opt_in' => 'boolean'];
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'marketing_opt_in' => 'boolean',
|
||||
'trial_ends_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function businesses()
|
||||
public function businesses(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Business::class)->withPivot(['role'])->withTimestamps();
|
||||
}
|
||||
|
||||
public function djProfile()
|
||||
public function djProfile(): HasOne
|
||||
{
|
||||
return $this->hasOne(DjProfile::class);
|
||||
}
|
||||
|
||||
public function enquiries(): HasMany
|
||||
{
|
||||
return $this->hasMany(Enquiry::class);
|
||||
}
|
||||
|
||||
public function bookings(): HasMany
|
||||
{
|
||||
return $this->hasMany(Booking::class, 'client_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -4,6 +4,9 @@ use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Middleware\PermissionMiddleware;
|
||||
use Spatie\Permission\Middleware\RoleMiddleware;
|
||||
use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
@@ -13,7 +16,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->validateCsrfTokens(except: ['stripe/*']);
|
||||
|
||||
$middleware->alias([
|
||||
'role' => RoleMiddleware::class,
|
||||
'permission' => PermissionMiddleware::class,
|
||||
'role_or_permission' => RoleOrPermissionMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\RsyncStudioLicensingServiceProvider;
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
RsyncStudioLicensingServiceProvider::class,
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
|
||||
+53
-34
@@ -1,37 +1,56 @@
|
||||
{
|
||||
"name": "absdev/beatbook",
|
||||
"type": "project",
|
||||
"description": "Professional DJ marketplace on Laravel 13",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/cashier": "^16.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"spatie/laravel-permission": "^7.0",
|
||||
"spatie/laravel-medialibrary": "^11.23.7",
|
||||
"predis/predis": "^3.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
"name": "absdev/beatbook",
|
||||
"type": "project",
|
||||
"description": "Professional DJ marketplace on Laravel 13",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/cashier": "^16.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"spatie/laravel-permission": "^7.0",
|
||||
"spatie/laravel-medialibrary": "^11.23.7",
|
||||
"predis/predis": "^3.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"deployer/deployer": "^8.0",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.41",
|
||||
"laravel/tinker": "^3.0",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"lint": [
|
||||
"./vendor/bin/pint --test"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => env('APP_NAME', 'BeatBook'),
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_GB'),
|
||||
'cipher' => 'AES-256-CBC',
|
||||
'key' => env('APP_KEY'),
|
||||
'previous_keys' => array_filter(explode(',', (string) env('APP_PREVIOUS_KEYS', ''))),
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
],
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
'default' => env('CACHE_STORE', env('CACHE_DRIVER', 'database')),
|
||||
'stores' => [
|
||||
'array' => ['driver' => 'array', 'serialize' => false],
|
||||
'database' => ['driver' => 'database', 'connection' => env('DB_CACHE_CONNECTION'), 'table' => env('DB_CACHE_TABLE', 'cache'), 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), 'lock_table' => env('DB_CACHE_LOCK_TABLE')],
|
||||
'file' => ['driver' => 'file', 'path' => storage_path('framework/cache/data'), 'lock_path' => storage_path('framework/cache/data')],
|
||||
'redis' => ['driver' => 'redis', 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default')],
|
||||
],
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
];
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
'connections' => [
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::ATTR_TIMEOUT => env('DB_TIMEOUT') ? (int) env('DB_TIMEOUT') : null,
|
||||
]) : [],
|
||||
],
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
],
|
||||
'migrations' => ['table' => 'migrations', 'update_date_on_publish' => true],
|
||||
'redis' => [
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
'disks' => [
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
],
|
||||
'links' => [public_path('storage') => storage_path('app/public')],
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
'deprecations' => ['channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false)],
|
||||
'channels' => [
|
||||
'stack' => ['driver' => 'stack', 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false],
|
||||
'single' => ['driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true],
|
||||
'daily' => ['driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'days' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true],
|
||||
'stderr' => ['driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, 'handler_with' => ['stream' => 'php://stderr'], 'processors' => [PsrLogMessageProcessor::class]],
|
||||
'syslog' => ['driver' => 'syslog', 'level' => env('LOG_LEVEL', 'debug'), 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 'replace_placeholders' => true],
|
||||
'errorlog' => ['driver' => 'errorlog', 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true],
|
||||
'null' => ['driver' => 'monolog', 'handler' => NullHandler::class],
|
||||
'emergency' => ['path' => storage_path('logs/laravel.log')],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
'mailers' => [
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
'log' => ['transport' => 'log', 'channel' => env('MAIL_LOG_CHANNEL')],
|
||||
'array' => ['transport' => 'array'],
|
||||
'failover' => ['transport' => 'failover', 'mailers' => ['smtp', 'log'], 'retry_after' => 60],
|
||||
],
|
||||
'from' => ['address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 'name' => env('MAIL_FROM_NAME', 'BeatBook')],
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
'connections' => [
|
||||
'sync' => ['driver' => 'sync'],
|
||||
'database' => ['driver' => 'database', 'connection' => env('DB_QUEUE_CONNECTION'), 'table' => env('DB_QUEUE_TABLE', 'jobs'), 'queue' => env('DB_QUEUE', 'default'), 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 'after_commit' => false],
|
||||
'redis' => ['driver' => 'redis', 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 'block_for' => null, 'after_commit' => false],
|
||||
],
|
||||
'batching' => ['database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'job_batches'],
|
||||
'failed' => ['driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'failed_jobs'],
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'postmark' => ['token' => env('POSTMARK_TOKEN')],
|
||||
'ses' => ['key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1')],
|
||||
'slack' => ['notifications' => ['bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL')]],
|
||||
'stripe' => ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET')],
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
'files' => storage_path('framework/sessions'),
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
'store' => env('SESSION_STORE'),
|
||||
'lottery' => [2, 100],
|
||||
'cookie' => env('SESSION_COOKIE', Str::slug((string) env('APP_NAME', 'laravel')).'-session'),
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
protected static ?string $password;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn () => ['email_verified_at' => null]);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ return new class extends Migration
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('avatar_path')->nullable();
|
||||
$table->boolean('marketing_opt_in')->default(false);
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
@@ -8,122 +8,132 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('businesses', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->string('name');
|
||||
$t->string('slug')->unique();
|
||||
$t->text('description')->nullable();
|
||||
$t->string('email')->nullable();
|
||||
$t->string('phone')->nullable();
|
||||
$t->string('website')->nullable();
|
||||
$t->string('status')->default('active');
|
||||
$t->timestamp('verified_at')->nullable();
|
||||
$t->timestamps();
|
||||
Schema::create('businesses', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('phone')->nullable();
|
||||
$table->string('website')->nullable();
|
||||
$table->string('status')->default('active')->index();
|
||||
$table->timestamp('verified_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('business_user', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->ulid('business_id');
|
||||
$t->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$t->string('role')->default('member');
|
||||
$t->timestamps();
|
||||
$t->unique(['business_id', 'user_id']);
|
||||
|
||||
Schema::create('business_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignUlid('business_id')->constrained('businesses')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('role')->default('member');
|
||||
$table->timestamps();
|
||||
$table->unique(['business_id', 'user_id']);
|
||||
});
|
||||
Schema::create('dj_profiles', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$t->ulid('business_id')->nullable();
|
||||
$t->string('stage_name');
|
||||
$t->string('slug')->unique();
|
||||
$t->string('headline')->nullable();
|
||||
$t->text('bio')->nullable();
|
||||
$t->string('city')->index();
|
||||
$t->unsignedSmallInteger('service_radius_miles')->default(30);
|
||||
$t->decimal('price_from', 10, 2)->nullable();
|
||||
$t->string('profile_photo')->nullable();
|
||||
$t->string('cover_photo')->nullable();
|
||||
$t->timestamp('verified_at')->nullable();
|
||||
$t->boolean('is_featured')->default(false);
|
||||
$t->boolean('is_published')->default(false);
|
||||
$t->decimal('average_rating', 3, 2)->default(0);
|
||||
$t->unsignedInteger('review_count')->default(0);
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('dj_profiles', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('business_id')->nullable()->constrained('businesses')->nullOnDelete();
|
||||
$table->string('stage_name');
|
||||
$table->string('slug')->unique();
|
||||
$table->string('headline')->nullable();
|
||||
$table->text('bio')->nullable();
|
||||
$table->string('city')->index();
|
||||
$table->unsignedSmallInteger('service_radius_miles')->default(30);
|
||||
$table->decimal('price_from', 10, 2)->nullable();
|
||||
$table->string('profile_photo')->nullable();
|
||||
$table->string('cover_photo')->nullable();
|
||||
$table->timestamp('verified_at')->nullable();
|
||||
$table->boolean('is_featured')->default(false)->index();
|
||||
$table->boolean('is_published')->default(false)->index();
|
||||
$table->decimal('average_rating', 3, 2)->default(0);
|
||||
$table->unsignedInteger('review_count')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('genres', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->string('name');
|
||||
$t->string('slug')->unique();
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('genres', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('dj_profile_genre', function (Blueprint $t) {
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->foreignId('genre_id')->constrained()->cascadeOnDelete();
|
||||
$t->primary(['dj_profile_id', 'genre_id']);
|
||||
|
||||
Schema::create('dj_profile_genre', function (Blueprint $table) {
|
||||
$table->foreignUlid('dj_profile_id')->constrained('dj_profiles')->cascadeOnDelete();
|
||||
$table->foreignId('genre_id')->constrained()->cascadeOnDelete();
|
||||
$table->primary(['dj_profile_id', 'genre_id']);
|
||||
});
|
||||
Schema::create('enquiries', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained();
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->string('event_type');
|
||||
$t->date('event_date');
|
||||
$t->time('start_time')->nullable();
|
||||
$t->time('end_time')->nullable();
|
||||
$t->string('venue');
|
||||
$t->string('postcode')->nullable();
|
||||
$t->unsignedInteger('guest_count')->nullable();
|
||||
$t->decimal('budget', 10, 2)->nullable();
|
||||
$t->text('message');
|
||||
$t->string('status')->default('new')->index();
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('enquiries', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('dj_profile_id')->constrained('dj_profiles')->cascadeOnDelete();
|
||||
$table->string('event_type');
|
||||
$table->date('event_date')->index();
|
||||
$table->time('start_time')->nullable();
|
||||
$table->time('end_time')->nullable();
|
||||
$table->string('venue');
|
||||
$table->string('postcode')->nullable();
|
||||
$table->unsignedInteger('guest_count')->nullable();
|
||||
$table->decimal('budget', 10, 2)->nullable();
|
||||
$table->text('message');
|
||||
$table->string('status')->default('new')->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('bookings', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->ulid('enquiry_id')->nullable();
|
||||
$t->foreignId('client_id')->constrained('users');
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->date('event_date');
|
||||
$t->decimal('total', 10, 2);
|
||||
$t->decimal('deposit', 10, 2)->default(0);
|
||||
$t->string('status')->default('pending')->index();
|
||||
$t->timestamp('paid_at')->nullable();
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('bookings', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('enquiry_id')->nullable()->constrained('enquiries')->nullOnDelete();
|
||||
$table->foreignId('client_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignUlid('dj_profile_id')->constrained('dj_profiles')->cascadeOnDelete();
|
||||
$table->date('event_date')->index();
|
||||
$table->decimal('total', 10, 2);
|
||||
$table->decimal('deposit', 10, 2)->default(0);
|
||||
$table->string('status')->default('pending')->index();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('reviews', function (Blueprint $t) {
|
||||
$t->ulid('id')->primary();
|
||||
$t->ulid('booking_id');
|
||||
$t->foreignId('user_id')->constrained();
|
||||
$t->ulid('dj_profile_id');
|
||||
$t->unsignedTinyInteger('rating');
|
||||
$t->string('title')->nullable();
|
||||
$t->text('body');
|
||||
$t->string('status')->default('pending')->index();
|
||||
$t->text('dj_reply')->nullable();
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('reviews', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->foreignUlid('booking_id')->constrained('bookings')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('dj_profile_id')->constrained('dj_profiles')->cascadeOnDelete();
|
||||
$table->unsignedTinyInteger('rating');
|
||||
$table->string('title')->nullable();
|
||||
$table->text('body');
|
||||
$table->string('status')->default('pending')->index();
|
||||
$table->text('dj_reply')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['booking_id', 'user_id']);
|
||||
});
|
||||
Schema::create('cookie_consents', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->string('anonymous_id')->nullable()->index();
|
||||
$t->boolean('analytics')->default(false);
|
||||
$t->boolean('marketing')->default(false);
|
||||
$t->string('policy_version');
|
||||
$t->timestamp('consented_at');
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('cookie_consents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('anonymous_id')->nullable()->index();
|
||||
$table->boolean('analytics')->default(false);
|
||||
$table->boolean('marketing')->default(false);
|
||||
$table->string('policy_version');
|
||||
$table->timestamp('consented_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('audit_logs', function (Blueprint $t) {
|
||||
$t->id();
|
||||
$t->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$t->string('action')->index();
|
||||
$t->nullableMorphs('subject');
|
||||
$t->json('meta')->nullable();
|
||||
$t->string('ip_hash', 64)->nullable();
|
||||
$t->timestamps();
|
||||
|
||||
Schema::create('audit_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('action')->index();
|
||||
$table->nullableMorphs('subject');
|
||||
$table->json('meta')->nullable();
|
||||
$table->string('ip_hash', 64)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach (['audit_logs', 'cookie_consents', 'reviews', 'bookings', 'enquiries', 'dj_profile_genre', 'genres', 'dj_profiles', 'business_user', 'businesses'] as $x) {
|
||||
Schema::dropIfExists($x);
|
||||
foreach (['audit_logs', 'cookie_consents', 'reviews', 'bookings', 'enquiries', 'dj_profile_genre', 'genres', 'dj_profiles', 'business_user', 'businesses'] as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
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('permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
|
||||
Schema::create('roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('guard_name');
|
||||
$table->timestamps();
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
|
||||
Schema::create('model_has_permissions', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('permission_id');
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger('model_id');
|
||||
$table->index(['model_id', 'model_type']);
|
||||
$table->foreign('permission_id')->references('id')->on('permissions')->cascadeOnDelete();
|
||||
$table->primary(['permission_id', 'model_id', 'model_type']);
|
||||
});
|
||||
|
||||
Schema::create('model_has_roles', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('role_id');
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger('model_id');
|
||||
$table->index(['model_id', 'model_type']);
|
||||
$table->foreign('role_id')->references('id')->on('roles')->cascadeOnDelete();
|
||||
$table->primary(['role_id', 'model_id', 'model_type']);
|
||||
});
|
||||
|
||||
Schema::create('role_has_permissions', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('permission_id');
|
||||
$table->unsignedBigInteger('role_id');
|
||||
$table->foreign('permission_id')->references('id')->on('permissions')->cascadeOnDelete();
|
||||
$table->foreign('role_id')->references('id')->on('roles')->cascadeOnDelete();
|
||||
$table->primary(['permission_id', 'role_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('role_has_permissions');
|
||||
Schema::dropIfExists('model_has_roles');
|
||||
Schema::dropIfExists('model_has_permissions');
|
||||
Schema::dropIfExists('roles');
|
||||
Schema::dropIfExists('permissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
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) {
|
||||
$table->string('stripe_id')->nullable()->index();
|
||||
$table->string('pm_type')->nullable();
|
||||
$table->string('pm_last_four', 4)->nullable();
|
||||
$table->timestamp('trial_ends_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('subscriptions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$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']);
|
||||
});
|
||||
|
||||
Schema::create('subscription_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('subscription_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('stripe_id')->unique();
|
||||
$table->string('stripe_product');
|
||||
$table->string('stripe_price');
|
||||
$table->integer('quantity')->nullable();
|
||||
$table->string('meter_event_name')->nullable();
|
||||
$table->string('meter_id')->nullable();
|
||||
$table->timestamps();
|
||||
$table->index(['subscription_id', 'stripe_price']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subscription_items');
|
||||
Schema::dropIfExists('subscriptions');
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropIndex(['stripe_id']);
|
||||
$table->dropColumn(['stripe_id', 'pm_type', 'pm_last_four', 'trial_ends_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,7 @@ class DatabaseSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
foreach (['super-admin', 'admin', 'dj', 'client', 'moderator', 'support'] as $r) {
|
||||
Role::firstOrCreate(['name' => $r]);
|
||||
Role::firstOrCreate(['name' => $r, 'guard_name' => 'web']);
|
||||
} $admin = User::firstOrCreate(['email' => 'admin@beatbook.test'], ['name' => 'BeatBook Admin', 'password' => 'password']);
|
||||
$admin->assignRole('admin');
|
||||
$djUser = User::firstOrCreate(['email' => 'dj@beatbook.test'], ['name' => 'Luna Hart', 'password' => 'password']);
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Unit"><directory>tests/Unit</directory></testsuite>
|
||||
<testsuite name="Feature"><directory>tests/Feature</directory></testsuite>
|
||||
</testsuites>
|
||||
<source><include><directory>app</directory></include></source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing" force="true"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"resources/css/app.css": {
|
||||
"file": "assets/app-DkQDD-ks.css",
|
||||
"file": "assets/app-4ylyCDk1.css",
|
||||
"src": "resources/css/app.css",
|
||||
"isEntry": true,
|
||||
"name": "app",
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
:root{--ink:#070a19;--panel:#0d1228;--purple:#7c3aed;--pink:#ec4899;--cyan:#22d3ee;--gold:#fbbf24}*{box-sizing:border-box}body{margin:0;background:#070a19;color:#f8fafc;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.shell{max-width:1280px;margin:auto;padding:0 24px}.nav{height:76px;display:flex;align-items:center;justify-content:space-between}.brand{font-size:25px;font-weight:900;color:white;text-decoration:none}.brand b{color:#ec4899}.navlinks{display:flex;gap:28px}.navlinks a{color:#dbeafe;text-decoration:none;font-size:14px}.button{display:inline-flex;padding:13px 20px;border-radius:12px;background:linear-gradient(135deg,var(--purple),var(--pink));color:white;text-decoration:none;border:0;font-weight:800}.button.ghost{background:transparent;border:1px solid #334155}.hero{min-height:610px;background:linear-gradient(90deg,rgba(7,10,25,.97) 0%,rgba(7,10,25,.72) 50%,rgba(7,10,25,.25)),url('https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=2000&q=85') center/cover;border-bottom:1px solid #202743}.hero-copy{padding:105px 0 35px;max-width:760px}.eyebrow{color:#67e8f9;font-weight:800;letter-spacing:.18em;text-transform:uppercase}.hero h1{font-size:72px;line-height:.95;margin:14px 0 22px;letter-spacing:-.05em}.hero h1 span{background:linear-gradient(90deg,#c084fc,#f472b6,#22d3ee);background-clip:text;color:transparent}.hero p{font-size:19px;color:#cbd5e1;max-width:620px}.searchbox{margin-top:34px;padding:10px;background:white;border-radius:18px;display:grid;grid-template-columns:1.2fr 1fr 1fr auto;gap:8px;box-shadow:0 25px 80px #0008}.searchbox input,.searchbox select{border:0;padding:17px;color:#0f172a;background:#f8fafc;border-radius:10px;outline:none}.section{padding:72px 0}.section-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.section h2{font-size:34px;margin:0}.muted{color:#94a3b8}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.card{background:#10162d;border:1px solid #27304d;border-radius:18px;overflow:hidden;box-shadow:0 14px 35px #0004}.card-img{height:235px;background:center/cover}.card-body{padding:18px}.card h3{margin:0 0 8px;font-size:20px}.stars{color:var(--gold)}.price{font-size:20px;font-weight:900;margin:14px 0}.chips{display:flex;gap:7px;flex-wrap:wrap}.chip{font-size:12px;padding:6px 9px;background:#1e293b;border-radius:99px;color:#cbd5e1}.profile-hero{height:350px;background:center/cover;position:relative}.profile-panel{margin:-90px auto 40px;position:relative;background:white;color:#0f172a;border-radius:22px;padding:30px;display:grid;grid-template-columns:130px 1fr 220px;gap:25px;box-shadow:0 30px 70px #0007}.avatar{width:130px;height:130px;border-radius:16px;object-fit:cover}.admin{display:grid;grid-template-columns:240px 1fr;min-height:100vh;background:#f8fafc;color:#0f172a}.sidebar{background:#0b1022;color:white;padding:28px 18px}.sidebar a{display:block;color:#cbd5e1;text-decoration:none;padding:11px;border-radius:8px}.sidebar a:hover{background:#1e293b}.admin-main{padding:35px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.stat{background:white;padding:24px;border-radius:14px;box-shadow:0 3px 15px #0f172a0d}.stat strong{display:block;font-size:30px;color:#7c3aed}.cookie{position:fixed;bottom:18px;left:18px;right:18px;max-width:650px;background:#fff;color:#0f172a;border-radius:16px;padding:18px;box-shadow:0 20px 60px #0008;z-index:50}@media(max-width:900px){.navlinks{display:none}.hero h1{font-size:50px}.searchbox{grid-template-columns:1fr}.cards{grid-template-columns:1fr 1fr}.profile-panel{grid-template-columns:1fr;margin:-40px 16px 30px}.admin{grid-template-columns:1fr}.sidebar{display:none}.stats{grid-template-columns:1fr 1fr}}@media(max-width:600px){.cards,.stats{grid-template-columns:1fr}.hero h1{font-size:42px}.hero-copy{padding-top:70px}}
|
||||
:root{--ink:#070a19;--panel:#0d1228;--purple:#7c3aed;--pink:#ec4899;--cyan:#22d3ee;--gold:#fbbf24}*{box-sizing:border-box}body{margin:0;background:#070a19;color:#f8fafc;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.shell{max-width:1280px;margin:auto;padding:0 24px}.nav{height:76px;display:flex;align-items:center;justify-content:space-between}.brand{font-size:25px;font-weight:900;color:white;text-decoration:none}.brand b{color:#ec4899}.navlinks{display:flex;gap:28px}.navlinks a{color:#dbeafe;text-decoration:none;font-size:14px}.button{display:inline-flex;padding:13px 20px;border-radius:12px;background:linear-gradient(135deg,var(--purple),var(--pink));color:white;text-decoration:none;border:0;font-weight:800}.button.ghost{background:transparent;border:1px solid #334155}.hero{min-height:610px;background:linear-gradient(90deg,rgba(7,10,25,.97) 0%,rgba(7,10,25,.72) 50%,rgba(7,10,25,.25)),url('https://images.unsplash.com/photo-1571266028243-d220c9c3b2d2?auto=format&fit=crop&w=2000&q=85') center/cover;border-bottom:1px solid #202743}.hero-copy{padding:105px 0 35px;max-width:760px}.eyebrow{color:#67e8f9;font-weight:800;letter-spacing:.18em;text-transform:uppercase}.hero h1{font-size:72px;line-height:.95;margin:14px 0 22px;letter-spacing:-.05em}.hero h1 span{background:linear-gradient(90deg,#c084fc,#f472b6,#22d3ee);background-clip:text;color:transparent}.hero p{font-size:19px;color:#cbd5e1;max-width:620px}.searchbox{margin-top:34px;padding:10px;background:white;border-radius:18px;display:grid;grid-template-columns:1.2fr 1fr 1fr auto;gap:8px;box-shadow:0 25px 80px #0008}.searchbox input,.searchbox select{border:0;padding:17px;color:#0f172a;background:#f8fafc;border-radius:10px;outline:none}.section{padding:72px 0}.section-head{display:flex;justify-content:space-between;align-items:end;margin-bottom:25px}.section h2{font-size:34px;margin:0}.muted{color:#94a3b8}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.card{background:#10162d;border:1px solid #27304d;border-radius:18px;overflow:hidden;box-shadow:0 14px 35px #0004}.card-img{height:235px;background:center/cover}.card-body{padding:18px}.card h3{margin:0 0 8px;font-size:20px}.stars{color:var(--gold)}.price{font-size:20px;font-weight:900;margin:14px 0}.chips{display:flex;gap:7px;flex-wrap:wrap}.chip{font-size:12px;padding:6px 9px;background:#1e293b;border-radius:99px;color:#cbd5e1}.profile-hero{height:350px;background:center/cover;position:relative}.profile-panel{margin:-90px auto 40px;position:relative;background:white;color:#0f172a;border-radius:22px;padding:30px;display:grid;grid-template-columns:130px 1fr 220px;gap:25px;box-shadow:0 30px 70px #0007}.avatar{width:130px;height:130px;border-radius:16px;object-fit:cover}.admin{display:grid;grid-template-columns:240px 1fr;min-height:100vh;background:#f8fafc;color:#0f172a}.sidebar{background:#0b1022;color:white;padding:28px 18px}.sidebar a{display:block;color:#cbd5e1;text-decoration:none;padding:11px;border-radius:8px}.sidebar a:hover{background:#1e293b}.admin-main{padding:35px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}.stat{background:white;padding:24px;border-radius:14px;box-shadow:0 3px 15px #0f172a0d}.stat strong{display:block;font-size:30px;color:#7c3aed}.cookie{position:fixed;bottom:18px;left:18px;right:18px;max-width:650px;background:#fff;color:#0f172a;border-radius:16px;padding:18px;box-shadow:0 20px 60px #0008;z-index:50}@media(max-width:900px){.navlinks{display:none}.hero h1{font-size:50px}.searchbox{grid-template-columns:1fr}.cards{grid-template-columns:1fr 1fr}.profile-panel{grid-template-columns:1fr;margin:-40px 16px 30px}.admin{grid-template-columns:1fr}.sidebar{display:none}.stats{grid-template-columns:1fr 1fr}}@media(max-width:600px){.cards,.stats{grid-template-columns:1fr}.hero h1{font-size:42px}.hero-copy{padding-top:70px}}
|
||||
.nav-actions{display:flex;gap:9px;align-items:center}.inline-form{display:inline;margin:0}.footer{border-top:1px solid #1e293b}.flash{margin-top:14px;padding:14px 18px;border-radius:12px}.flash.success{background:#064e3b}.cookie p{margin:6px 0 12px}.cookie-secondary{color:#0f172a}.auth-page{min-height:calc(100vh - 160px);padding:70px 24px;background:radial-gradient(circle at 20% 15%,#3b076455,transparent 35%),radial-gradient(circle at 85% 35%,#164e6355,transparent 32%),#070a19}.auth-card{width:min(100%,520px);margin:0 auto;background:#10162d;border:1px solid #27304d;border-radius:24px;padding:34px;box-shadow:0 28px 80px #0008}.auth-card h1{margin:10px 0 8px;font-size:36px}.auth-form{display:grid;gap:16px;margin-top:26px}.auth-form label{display:grid;gap:8px;font-weight:700;color:#e2e8f0}.auth-form input{width:100%;border:1px solid #334155;background:#0b1022;color:#f8fafc;border-radius:12px;padding:14px 15px;font:inherit;outline:none}.auth-form input:focus{border-color:#a855f7;box-shadow:0 0 0 3px #a855f722}.auth-form .check-row{display:flex;grid-template-columns:auto 1fr;align-items:center;font-weight:500}.auth-form .check-row input{width:auto}.button.full{justify-content:center;width:100%;cursor:pointer}.auth-links{display:flex;flex-direction:column;gap:10px;margin-top:22px;color:#94a3b8}.auth-links a{color:#67e8f9;text-decoration:none}.form-errors{margin-top:18px;padding:14px 16px;border:1px solid #7f1d1d;background:#450a0a;color:#fecaca;border-radius:12px;display:grid;gap:5px}
|
||||
@media(max-width:900px){.nav-actions{gap:6px}.nav-actions .button{padding:10px 12px}.auth-page{padding:40px 16px}}@media(max-width:600px){.nav{height:auto;min-height:70px;gap:12px}.nav-actions{flex-wrap:wrap;justify-content:flex-end}.auth-card{padding:24px}.auth-card h1{font-size:30px}}
|
||||
|
||||
@@ -1 +1,41 @@
|
||||
<!doctype html><html><head><meta name="viewport" content="width=device-width">@vite(['resources/css/app.css'])<title>BeatBook Admin</title></head><body><div class="admin"><aside class="sidebar"><div class="brand"><b>◉</b> BeatBook</div><p class="muted">Administration</p>@foreach(['Dashboard','Users','DJs & Businesses','Clients','Bookings','Payments','Reviews','Messages','Notifications','Support','Reports','Content','Roles & Permissions','Privacy','Settings'] as $x)<a href="#">{{ $x }}</a>@endforeach</aside><main class="admin-main"><h1>Dashboard</h1><p style="color:#64748b">Marketplace overview and moderation queue.</p><div class="stats">@foreach($stats as $k=>$v)<div class="stat"><span>{{ ucfirst($k) }}</span><strong>{{ number_format($v) }}</strong></div>@endforeach</div><div class="stat" style="margin-top:22px;min-height:330px"><h2>Operations</h2><p style="color:#64748b">Revenue charts, recent bookings, verification requests, support tickets and review moderation plug into this dashboard.</p><div style="height:180px;background:linear-gradient(180deg,#ede9fe,#fff);border-radius:12px;margin-top:30px;display:flex;align-items:end;padding:25px;gap:10px">@foreach([25,42,36,61,55,78,70,92,86,110,120] as $h)<i style="display:block;width:7%;height:{{ $h }}px;background:#7c3aed;border-radius:5px"></i>@endforeach</div></div></main></div></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>BeatBook Admin</title>
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="{{ route('admin.dashboard') }}"><b>◉</b> BeatBook</a>
|
||||
<p class="muted">Administration</p>
|
||||
<a href="{{ route('admin.dashboard') }}">Dashboard</a>
|
||||
@foreach([
|
||||
'users' => 'Users', 'djs' => 'DJs & Businesses', 'clients' => 'Clients',
|
||||
'bookings' => 'Bookings', 'payments' => 'Payments', 'reviews' => 'Reviews',
|
||||
'messages' => 'Messages', 'notifications' => 'Notifications', 'support' => 'Support',
|
||||
'reports' => 'Reports', 'content' => 'Content', 'roles' => 'Roles & Permissions',
|
||||
'privacy' => 'Privacy', 'settings' => 'Settings'
|
||||
] as $key => $label)
|
||||
<a href="{{ route('admin.section', $key) }}">{{ $label }}</a>
|
||||
@endforeach
|
||||
<a href="{{ route('home') }}">Back to site</a>
|
||||
</aside>
|
||||
<main class="admin-main">
|
||||
<h1>Dashboard</h1>
|
||||
<p style="color:#64748b">Marketplace overview and moderation queue.</p>
|
||||
<div class="stats">
|
||||
@foreach($stats as $key => $value)
|
||||
<div class="stat"><span>{{ ucfirst($key) }}</span><strong>{{ number_format($value) }}</strong></div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="stat" style="margin-top:22px;min-height:330px">
|
||||
<h2>Operations</h2>
|
||||
<p style="color:#64748b">Revenue charts, recent bookings, verification requests, support tickets and review moderation plug into this dashboard.</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{ ucwords(str_replace('-', ' ', $section)) }} — BeatBook Admin</title>
|
||||
@vite(['resources/css/app.css'])
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="{{ route('admin.dashboard') }}"><b>◉</b> BeatBook</a>
|
||||
<p class="muted">Administration</p>
|
||||
<a href="{{ route('admin.dashboard') }}">Dashboard</a>
|
||||
@foreach([
|
||||
'users' => 'Users', 'djs' => 'DJs & Businesses', 'clients' => 'Clients',
|
||||
'bookings' => 'Bookings', 'payments' => 'Payments', 'reviews' => 'Reviews',
|
||||
'messages' => 'Messages', 'notifications' => 'Notifications', 'support' => 'Support',
|
||||
'reports' => 'Reports', 'content' => 'Content', 'roles' => 'Roles & Permissions',
|
||||
'privacy' => 'Privacy', 'settings' => 'Settings'
|
||||
] as $key => $label)
|
||||
<a href="{{ route('admin.section', $key) }}">{{ $label }}</a>
|
||||
@endforeach
|
||||
<a href="{{ route('home') }}">Back to site</a>
|
||||
</aside>
|
||||
<main class="admin-main">
|
||||
<div class="eyebrow">Administration</div>
|
||||
<h1>{{ ucwords(str_replace('-', ' ', $section)) }}</h1>
|
||||
<p style="color:#64748b">This BeatBook administration section is connected and ready for its management workflow.</p>
|
||||
<div class="stats">
|
||||
@foreach($counts as $label => $value)
|
||||
<div class="stat"><span>{{ ucfirst($label) }}</span><strong>{{ number_format($value) }}</strong></div>
|
||||
@endforeach
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Reset password — BeatBook')
|
||||
|
||||
@section('content')
|
||||
<main class="auth-page">
|
||||
<section class="auth-card">
|
||||
<div class="eyebrow">Account recovery</div>
|
||||
<h1>Forgot your password?</h1>
|
||||
<p class="muted">Enter your email and we will send a password reset link.</p>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="form-errors">
|
||||
@foreach($errors->all() as $error)<div>{{ $error }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="post" action="{{ route('password.email') }}" class="auth-form">
|
||||
@csrf
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ old('email') }}" required autofocus autocomplete="email">
|
||||
</label>
|
||||
<button class="button full" type="submit">Email reset link</button>
|
||||
</form>
|
||||
<div class="auth-links"><a href="{{ route('login') }}">Back to sign in</a></div>
|
||||
</section>
|
||||
</main>
|
||||
@endsection
|
||||
@@ -0,0 +1,36 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Sign in — BeatBook')
|
||||
|
||||
@section('content')
|
||||
<main class="auth-page">
|
||||
<section class="auth-card">
|
||||
<div class="eyebrow">Welcome back</div>
|
||||
<h1>Sign in to BeatBook</h1>
|
||||
<p class="muted">Manage enquiries, bookings and your account.</p>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="form-errors">
|
||||
@foreach($errors->all() as $error)<div>{{ $error }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="post" action="{{ route('login') }}" class="auth-form">
|
||||
@csrf
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ old('email') }}" required autofocus autocomplete="email">
|
||||
</label>
|
||||
<label>Password
|
||||
<input type="password" name="password" required autocomplete="current-password">
|
||||
</label>
|
||||
<label class="check-row"><input type="checkbox" name="remember"> <span>Remember me</span></label>
|
||||
<button class="button full" type="submit">Sign in</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-links">
|
||||
<a href="{{ route('password.request') }}">Forgot your password?</a>
|
||||
<span>New to BeatBook? <a href="{{ route('register') }}">Create an account</a></span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@endsection
|
||||
@@ -0,0 +1,38 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Create account — BeatBook')
|
||||
|
||||
@section('content')
|
||||
<main class="auth-page">
|
||||
<section class="auth-card">
|
||||
<div class="eyebrow">Join BeatBook</div>
|
||||
<h1>Create your account</h1>
|
||||
<p class="muted">Start enquiring with DJs and managing your bookings.</p>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="form-errors">
|
||||
@foreach($errors->all() as $error)<div>{{ $error }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="post" action="{{ route('register') }}" class="auth-form">
|
||||
@csrf
|
||||
<label>Name
|
||||
<input type="text" name="name" value="{{ old('name') }}" required autofocus autocomplete="name">
|
||||
</label>
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ old('email') }}" required autocomplete="email">
|
||||
</label>
|
||||
<label>Password
|
||||
<input type="password" name="password" required autocomplete="new-password">
|
||||
</label>
|
||||
<label>Confirm password
|
||||
<input type="password" name="password_confirmation" required autocomplete="new-password">
|
||||
</label>
|
||||
<button class="button full" type="submit">Create account</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-links">Already registered? <a href="{{ route('login') }}">Sign in</a></div>
|
||||
</section>
|
||||
</main>
|
||||
@endsection
|
||||
@@ -0,0 +1,33 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Choose a new password — BeatBook')
|
||||
|
||||
@section('content')
|
||||
<main class="auth-page">
|
||||
<section class="auth-card">
|
||||
<div class="eyebrow">Account recovery</div>
|
||||
<h1>Choose a new password</h1>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="form-errors">
|
||||
@foreach($errors->all() as $error)<div>{{ $error }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="post" action="{{ route('password.store') }}" class="auth-form">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ $request->route('token') }}">
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ old('email', $request->email) }}" required autocomplete="email">
|
||||
</label>
|
||||
<label>New password
|
||||
<input type="password" name="password" required autocomplete="new-password">
|
||||
</label>
|
||||
<label>Confirm password
|
||||
<input type="password" name="password_confirmation" required autocomplete="new-password">
|
||||
</label>
|
||||
<button class="button full" type="submit">Reset password</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
@endsection
|
||||
@@ -1 +1 @@
|
||||
@extends('layouts.app') @section('title',$dj->stage_name.' — BeatBook') @section('content')<div class="profile-hero" style="background-image:linear-gradient(#0002,#0008),url('{{ $dj->cover_photo ?: $dj->profile_photo }}')"></div><main class="shell"><section class="profile-panel"><img class="avatar" src="{{ $dj->profile_photo }}" alt="{{ $dj->stage_name }}"><div><h1 style="margin:0">{{ $dj->stage_name }} @if($dj->verified_at)<span style="color:#2563eb">✓</span>@endif</h1><div class="stars">★★★★★ <span style="color:#64748b">{{ $dj->average_rating }} · {{ $dj->review_count }} reviews</span></div><p>{{ $dj->headline }}</p><div class="chips">@foreach($dj->genres as $genre)<span class="chip">{{ $genre->name }}</span>@endforeach</div></div><aside><span>From</span><div style="font-size:34px;font-weight:900">£{{ number_format($dj->price_from,0) }}</div><span>per event</span><br><br><a class="button" href="#enquire">Request a quote</a></aside></section><section class="section" style="padding-top:15px"><div style="display:grid;grid-template-columns:2fr 1fr;gap:35px"><div><h2>About {{ $dj->stage_name }}</h2><p class="muted" style="font-size:17px;line-height:1.8">{{ $dj->bio }}</p><h2>Reviews</h2>@foreach($dj->reviews as $review)<div class="card" style="padding:20px;margin-bottom:12px"><div class="stars">{{ str_repeat('★',$review->rating) }}</div><h3>{{ $review->title }}</h3><p>{{ $review->body }}</p></div>@endforeach</div><aside id="enquire" class="card" style="padding:22px;height:max-content"><h2>Check availability</h2>@auth<form method="post" action="{{ route('enquiries.store',$dj) }}">@csrf<input name="event_type" placeholder="Event type" required style="width:100%;padding:12px;margin:5px 0"><input name="event_date" type="date" required style="width:100%;padding:12px;margin:5px 0"><input name="venue" placeholder="Venue / town" required style="width:100%;padding:12px;margin:5px 0"><input name="budget" type="number" placeholder="Budget £" style="width:100%;padding:12px;margin:5px 0"><textarea name="message" required placeholder="Tell the DJ about your event" style="width:100%;min-height:130px;padding:12px;margin:5px 0"></textarea><button class="button">Send enquiry</button></form>@else<p class="muted">Sign in to send a secure enquiry.</p><a class="button" href="/login">Sign in</a>@endauth</aside></div></section></main>@endsection
|
||||
@extends('layouts.app') @section('title',$dj->stage_name.' — BeatBook') @section('content')<div class="profile-hero" style="background-image:linear-gradient(#0002,#0008),url('{{ $dj->cover_photo ?: $dj->profile_photo }}')"></div><main class="shell"><section class="profile-panel"><img class="avatar" src="{{ $dj->profile_photo }}" alt="{{ $dj->stage_name }}"><div><h1 style="margin:0">{{ $dj->stage_name }} @if($dj->verified_at)<span style="color:#2563eb">✓</span>@endif</h1><div class="stars">★★★★★ <span style="color:#64748b">{{ $dj->average_rating }} · {{ $dj->review_count }} reviews</span></div><p>{{ $dj->headline }}</p><div class="chips">@foreach($dj->genres as $genre)<span class="chip">{{ $genre->name }}</span>@endforeach</div></div><aside><span>From</span><div style="font-size:34px;font-weight:900">£{{ number_format($dj->price_from,0) }}</div><span>per event</span><br><br><a class="button" href="#enquire">Request a quote</a></aside></section><section class="section" style="padding-top:15px"><div style="display:grid;grid-template-columns:2fr 1fr;gap:35px"><div><h2>About {{ $dj->stage_name }}</h2><p class="muted" style="font-size:17px;line-height:1.8">{{ $dj->bio }}</p><h2>Reviews</h2>@foreach($dj->reviews as $review)<div class="card" style="padding:20px;margin-bottom:12px"><div class="stars">{{ str_repeat('★',$review->rating) }}</div><h3>{{ $review->title }}</h3><p>{{ $review->body }}</p></div>@endforeach</div><aside id="enquire" class="card" style="padding:22px;height:max-content"><h2>Check availability</h2>@auth<form method="post" action="{{ route('enquiries.store',$dj) }}">@csrf<input name="event_type" placeholder="Event type" required style="width:100%;padding:12px;margin:5px 0"><input name="event_date" type="date" required style="width:100%;padding:12px;margin:5px 0"><input name="venue" placeholder="Venue / town" required style="width:100%;padding:12px;margin:5px 0"><input name="budget" type="number" placeholder="Budget £" style="width:100%;padding:12px;margin:5px 0"><textarea name="message" required placeholder="Tell the DJ about your event" style="width:100%;min-height:130px;padding:12px;margin:5px 0"></textarea><button class="button">Send enquiry</button></form>@else<p class="muted">Sign in to send a secure enquiry.</p><a class="button" href="{{ route('login') }}">Sign in</a>@endauth</aside></div></section></main>@endsection
|
||||
@@ -1 +1 @@
|
||||
@extends('layouts.app') @section('content')<section class="hero"><div class="shell hero-copy"><div class="eyebrow">Music creates memories</div><h1>Find your <span>perfect DJ</span></h1><p>Discover trusted DJs for weddings, parties, corporate events and clubs. Compare profiles, reviews and prices, then enquire with confidence.</p><form class="searchbox" action="{{ route('djs.index') }}"><input name="location" placeholder="Town or postcode"><select name="event_type"><option value="">Event type</option><option>Wedding</option><option>Party</option><option>Corporate</option><option>Club</option></select><input type="date" name="date"><button class="button">Search DJs</button></form></div></section><section class="section"><div class="shell"><div class="section-head"><div><div class="eyebrow">Top talent</div><h2>Featured DJs</h2></div><a class="button ghost" href="{{ route('djs.index') }}">View all DJs</a></div><div class="cards">@forelse($featured as $dj) @include('djs._card',['dj'=>$dj]) @empty @foreach([['DJ Luna','London',350,'photo-1516280440614-37939bbacd81'],['DJ Maverick','Manchester',400,'photo-1571266028243-d220c9c3b2d2'],['DJ Rhythm','Birmingham',300,'photo-1524368535928-5b5e00ddc76b'],['DJ Serenity','Leeds',320,'photo-1506157786151-b8491531f063']] as $d)<article class="card"><div class="card-img" style="background-image:url('https://images.unsplash.com/{{ $d[3] }}?auto=format&fit=crop&w=700&q=80')"></div><div class="card-body"><h3>{{ $d[0] }}</h3><div class="stars">★★★★★ <span class="muted">4.9</span></div><p class="muted">{{ $d[1] }} · Weddings · Parties</p><div class="price">From £{{ $d[2] }}</div><a class="button" href="{{ route('djs.index') }}">View profile</a></div></article>@endforeach @endforelse</div></div></section><section id="how" class="section" style="background:#0d1228"><div class="shell"><div class="eyebrow">Simple & secure</div><h2>From search to dance floor</h2><div class="cards" style="margin-top:30px">@foreach(['Search'=>'Find DJs by location, date, event and budget.','Compare'=>'Review portfolios, pricing and verified feedback.','Enquire'=>'Send your event details without exposing private contact data.','Book & review'=>'Confirm securely and review after the event.'] as $a=>$b)<div class="card"><div class="card-body"><h3>{{ $a }}</h3><p class="muted">{{ $b }}</p></div></div>@endforeach</div></div></section>@endsection
|
||||
@extends('layouts.app') @section('content')<section class="hero"><div class="shell hero-copy"><div class="eyebrow">Music creates memories</div><h1>Find your <span>perfect DJ</span></h1><p>Discover trusted DJs for weddings, parties, corporate events and clubs. Compare profiles, reviews and prices, then enquire with confidence.</p><form class="searchbox" action="{{ route('djs.index') }}"><input name="location" placeholder="Town or postcode"><select name="event_type"><option value="">Event type</option><option>Wedding</option><option>Party</option><option>Corporate</option><option>Club</option></select><input type="date" name="date"><button class="button">Search DJs</button></form></div></section><section class="section"><div class="shell"><div class="section-head"><div><div class="eyebrow">Top talent</div><h2>Featured DJs</h2></div><a class="button ghost" href="{{ route('djs.index') }}">View all DJs</a></div><div class="cards">@forelse($featured as $dj) @include('djs._card',['dj'=>$dj]) @empty @foreach([['DJ Luna','London',350,'photo-1516280440614-37939bbacd81'],['DJ Maverick','Manchester',400,'photo-1571266028243-d220c9c3b2d2'],['DJ Rhythm','Birmingham',300,'photo-1524368535928-5b5e00ddc76b'],['DJ Serenity','Leeds',320,'photo-1506157786151-b8491531f063']] as $d)<article class="card"><div class="card-img" style="background-image:url('https://images.unsplash.com/{{ $d[3] }}?auto=format&fit=crop&w=700&q=80')"></div><div class="card-body"><h3>{{ $d[0] }}</h3><div class="stars">★★★★★ <span class="muted">4.9</span></div><p class="muted">{{ $d[1] }} · Weddings · Parties</p><div class="price">From £{{ $d[2] }}</div><a class="button" href="{{ route('djs.index') }}">View profile</a></div></article>@endforeach @endforelse</div></div></section><section id="how" class="section" style="background:#0d1228"><div class="shell"><div class="eyebrow">Simple & secure</div><h2>From search to dance floor</h2><div class="cards" style="margin-top:30px">@foreach(['Search'=>'Find DJs by location, date, event and budget.','Compare'=>'Review portfolios, pricing and verified feedback.','Enquire'=>'Send your event details without exposing private contact data.','Book & review'=>'Confirm securely and review after the event.'] as $a=>$b)<div class="card"><div class="card-body"><h3>{{ $a }}</h3><p class="muted">{{ $b }}</p></div></div>@endforeach</div></div></section><section id="for-djs" class="section"><div class="shell"><div class="section-head"><div><div class="eyebrow">For professionals</div><h2>Grow your DJ business</h2></div></div><div class="cards"><div class="card"><div class="card-body"><h3>Professional profile</h3><p class="muted">Show your sound, experience, service area and pricing in one trusted profile.</p></div></div><div class="card"><div class="card-body"><h3>Qualified enquiries</h3><p class="muted">Receive structured event details so you can quote quickly and accurately.</p></div></div><div class="card"><div class="card-body"><h3>Verified reviews</h3><p class="muted">Build reputation from completed BeatBook bookings.</p></div></div><div class="card"><div class="card-body"><h3>Business tools</h3><p class="muted">Manage bookings, payments and customer communication from your dashboard.</p></div></div></div></div></section><section id="pricing" class="section" style="background:#0d1228"><div class="shell"><div class="eyebrow">Straightforward pricing</div><h2>Built for clients and DJs</h2><p class="muted" style="max-width:720px">Browsing and creating a client account is free. DJ subscription and payment options can be configured from the BeatBook billing area as the marketplace launches.</p></div></section>@endsection
|
||||
@@ -1 +1,70 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Find and book outstanding DJs for weddings, parties, clubs and corporate events."><title>@yield('title','BeatBook — Find Your Perfect DJ')</title>@vite(['resources/css/app.css','resources/js/app.js'])</head><body><header><div class="shell nav"><a class="brand" href="{{ route('home') }}"><b>◉</b> BeatBook</a><nav class="navlinks"><a href="{{ route('djs.index') }}">Find a DJ</a><a href="/#how">How it works</a><a href="/#for-djs">For DJs</a><a href="/#pricing">Pricing</a></nav><div style="display:flex;gap:9px">@auth<a class="button ghost" href="{{ route('dashboard') }}">Dashboard</a>@else<a class="button ghost" href="/login">Sign in</a><a class="button" href="/register">Join</a>@endauth</div></div></header>@if(session('success'))<div class="shell" style="padding:14px;background:#064e3b;border-radius:12px">{{ session('success') }}</div>@endif>@yield('content')<footer class="section" style="border-top:1px solid #1e293b"><div class="shell"><b class="brand">BeatBook</b><p class="muted">DJs for every moment. Secure enquiries, bookings and verified reviews.</p><p><a href="{{ route('privacy') }}" class="muted">Privacy</a> · <a href="{{ route('cookies') }}" class="muted">Cookies</a> · <a href="{{ route('terms') }}" class="muted">Terms</a></p></div></footer><div class="cookie" id="cookie"><strong>Your privacy matters</strong><p style="margin:6px 0 12px">We use necessary cookies to run BeatBook. Analytics and marketing remain off until you choose them.</p><button class="button" onclick="localStorage.setItem('cookieChoice','necessary');document.getElementById('cookie').remove()">Necessary only</button> <button class="button ghost" style="color:#0f172a" onclick="localStorage.setItem('cookieChoice','all');document.getElementById('cookie').remove()">Accept all</button></div><script>if(localStorage.getItem('cookieChoice'))document.getElementById('cookie')?.remove()</script></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="description" content="Find and book outstanding DJs for weddings, parties, clubs and corporate events.">
|
||||
<title>@yield('title', 'BeatBook — Find Your Perfect DJ')</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="shell nav">
|
||||
<a class="brand" href="{{ route('home') }}"><b>◉</b> BeatBook</a>
|
||||
<nav class="navlinks">
|
||||
<a href="{{ route('djs.index') }}">Find a DJ</a>
|
||||
<a href="{{ route('home') }}#how">How it works</a>
|
||||
<a href="{{ route('home') }}#for-djs">For DJs</a>
|
||||
<a href="{{ route('home') }}#pricing">Pricing</a>
|
||||
</nav>
|
||||
<div class="nav-actions">
|
||||
@auth
|
||||
@if(auth()->user()->hasAnyRole(['admin', 'super-admin']))
|
||||
<a class="button ghost" href="{{ route('admin.dashboard') }}">Admin</a>
|
||||
@endif
|
||||
<a class="button ghost" href="{{ route('dashboard') }}">Dashboard</a>
|
||||
<form method="post" action="{{ route('logout') }}" class="inline-form">
|
||||
@csrf
|
||||
<button class="button" type="submit">Sign out</button>
|
||||
</form>
|
||||
@else
|
||||
<a class="button ghost" href="{{ route('login') }}">Sign in</a>
|
||||
<a class="button" href="{{ route('register') }}">Join</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="shell flash success">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('status'))
|
||||
<div class="shell flash success">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
|
||||
<footer class="section footer">
|
||||
<div class="shell">
|
||||
<b class="brand">BeatBook</b>
|
||||
<p class="muted">DJs for every moment. Secure enquiries, bookings and verified reviews.</p>
|
||||
<p>
|
||||
<a href="{{ route('privacy') }}" class="muted">Privacy</a> ·
|
||||
<a href="{{ route('cookies') }}" class="muted">Cookies</a> ·
|
||||
<a href="{{ route('terms') }}" class="muted">Terms</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="cookie" id="cookie">
|
||||
<strong>Your privacy matters</strong>
|
||||
<p>We use necessary cookies to run BeatBook. Analytics and marketing remain off until you choose them.</p>
|
||||
<button class="button" type="button" onclick="localStorage.setItem('cookieChoice','necessary');document.getElementById('cookie').remove()">Necessary only</button>
|
||||
<button class="button ghost cookie-secondary" type="button" onclick="localStorage.setItem('cookieChoice','all');document.getElementById('cookie').remove()">Accept all</button>
|
||||
</div>
|
||||
<script>
|
||||
if (localStorage.getItem('cookieChoice')) document.getElementById('cookie')?.remove();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment('BeatBook');
|
||||
})->purpose('Display the application name');
|
||||
+22
-1
@@ -1,6 +1,11 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\SectionController;
|
||||
use App\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||
use App\Http\Controllers\Auth\NewPasswordController;
|
||||
use App\Http\Controllers\Auth\PasswordResetLinkController;
|
||||
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||
use App\Http\Controllers\DjController;
|
||||
use App\Http\Controllers\EnquiryController;
|
||||
use App\Http\Controllers\HomeController;
|
||||
@@ -10,14 +15,30 @@ use Illuminate\Support\Facades\Route;
|
||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||
Route::get('/djs', [DjController::class, 'index'])->name('djs.index');
|
||||
Route::get('/dj/{djProfile:slug}', [DjController::class, 'show'])->name('djs.show');
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', [AuthenticatedSessionController::class, 'create'])->name('login');
|
||||
Route::post('/login', [AuthenticatedSessionController::class, 'store']);
|
||||
Route::get('/register', [RegisteredUserController::class, 'create'])->name('register');
|
||||
Route::post('/register', [RegisteredUserController::class, 'store']);
|
||||
Route::get('/forgot-password', [PasswordResetLinkController::class, 'create'])->name('password.request');
|
||||
Route::post('/forgot-password', [PasswordResetLinkController::class, 'store'])->name('password.email');
|
||||
Route::get('/reset-password/{token}', [NewPasswordController::class, 'create'])->name('password.reset');
|
||||
Route::post('/reset-password', [NewPasswordController::class, 'store'])->name('password.store');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->group(function () {
|
||||
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
|
||||
Route::post('/dj/{djProfile}/enquire', [EnquiryController::class, 'store'])->name('enquiries.store');
|
||||
Route::post('/bookings/{booking}/reviews', [ReviewController::class, 'store'])->name('reviews.store');
|
||||
Route::view('/dashboard', 'client.dashboard')->name('dashboard');
|
||||
});
|
||||
|
||||
Route::middleware(['auth', 'role:admin|super-admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/', DashboardController::class)->name('dashboard');
|
||||
Route::get('/{section}', SectionController::class)->name('section');
|
||||
});
|
||||
|
||||
Route::view('/privacy', 'legal.privacy')->name('privacy');
|
||||
Route::view('/cookies', 'legal.cookies')->name('cookies');
|
||||
Route::view('/terms', 'legal.terms')->name('terms');
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_user_can_register(): void
|
||||
{
|
||||
Role::create(['name' => 'client', 'guard_name' => 'web']);
|
||||
|
||||
$response = $this->post('/register', [
|
||||
'name' => 'Test Client',
|
||||
'email' => 'client@example.test',
|
||||
'password' => 'password12345',
|
||||
'password_confirmation' => 'password12345',
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/dashboard');
|
||||
$this->assertAuthenticated();
|
||||
$this->assertDatabaseHas('users', ['email' => 'client@example.test']);
|
||||
}
|
||||
|
||||
public function test_user_can_login_and_logout(): void
|
||||
{
|
||||
$user = User::factory()->create(['password' => 'password12345']);
|
||||
|
||||
$this->post('/login', ['email' => $user->email, 'password' => 'password12345'])
|
||||
->assertRedirect('/dashboard');
|
||||
$this->assertAuthenticatedAs($user);
|
||||
|
||||
$this->post('/logout')->assertRedirect('/');
|
||||
$this->assertGuest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PublicRoutesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_public_pages_render(): void
|
||||
{
|
||||
foreach (['/', '/djs', '/login', '/register', '/forgot-password', '/privacy', '/cookies', '/terms', '/up'] as $uri) {
|
||||
$this->get($uri)->assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_dashboard_redirects_guests_to_login(): void
|
||||
{
|
||||
$this->get('/dashboard')->assertRedirect('/login');
|
||||
}
|
||||
|
||||
public function test_admin_redirects_guests_to_login(): void
|
||||
{
|
||||
$this->get('/admin')->assertRedirect('/login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
Reference in New Issue
Block a user