29 files changed
+94
-881
No files matched your search
@@ -42,10 +42,6 @@ class AuthController extends Controller
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
if ($request->user()->isSuperAdmin) {
|
||||
return redirect()->intended(route('manager.dashboard'));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('customer.dashboard'));
|
||||
}
|
||||
|
||||
@@ -104,11 +100,10 @@ class AuthController extends Controller
|
||||
|
||||
public function logout(Request $request): RedirectResponse
|
||||
{
|
||||
$wasSuperAdmin = $request->user()?->isSuperAdmin === true;
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route($wasSuperAdmin ? 'manager.login' : 'public.home');
|
||||
return redirect()->route('public.home');
|
||||
}
|
||||
}
|
||||
@@ -2,75 +2,39 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\InstallationStatus;
|
||||
use App\Models\ApplicationInstallation;
|
||||
use App\Models\User;
|
||||
use App\Models\{ApplicationInstallation, BookingRule, Business, BusinessDomain, BusinessSettings, Role, RolePermission, StaffProfile, User};
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class InstallController extends Controller
|
||||
{
|
||||
public function create(Request $request): View|RedirectResponse
|
||||
public function create(): View|RedirectResponse
|
||||
{
|
||||
if ($this->isInstalled()) {
|
||||
return $request->user()?->isSuperAdmin
|
||||
? redirect()->route('manager.dashboard')
|
||||
: redirect()->route('manager.login');
|
||||
}
|
||||
|
||||
if (ApplicationInstallation::query()->where('status', InstallationStatus::Installed)->exists()) return redirect()->route('public.home');
|
||||
return view('install');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
if ($this->isInstalled()) {
|
||||
return redirect()->route('manager.login');
|
||||
}
|
||||
if (ApplicationInstallation::query()->where('status', InstallationStatus::Installed)->exists()) return redirect()->route('public.home');
|
||||
$data = $request->validate(['businessName' => ['required', 'string', 'max:190'], 'slug' => ['required', 'alpha_dash', 'max:80', 'unique:Business,slug'], 'adminName' => ['required', 'string', 'max:190'], 'adminEmail' => ['required', 'email', 'max:190', 'unique:User,email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'domain' => ['nullable', 'string', 'max:190']]);
|
||||
|
||||
$data = $request->validate([
|
||||
'adminName' => ['required', 'string', 'max:160'],
|
||||
'adminEmail' => ['required', 'email', 'max:190', 'unique:User,email'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
]);
|
||||
|
||||
$admin = DB::transaction(function () use ($data): User {
|
||||
$admin = User::create([
|
||||
'name' => trim($data['adminName']),
|
||||
'email' => strtolower(trim($data['adminEmail'])),
|
||||
'passwordHash' => Hash::make($data['password']),
|
||||
'isSuperAdmin' => true,
|
||||
'emailVerified' => now(),
|
||||
]);
|
||||
|
||||
ApplicationInstallation::create([
|
||||
'id' => 'singleton',
|
||||
'status' => InstallationStatus::Installed,
|
||||
'installedAt' => now(),
|
||||
'installedVersion' => 'laravel-1',
|
||||
'initialAdminUserId' => $admin->id,
|
||||
'primaryBusinessId' => null,
|
||||
'metadata' => ['installer' => 'platform-admin-only'],
|
||||
]);
|
||||
|
||||
return $admin;
|
||||
DB::transaction(function () use ($data, $request): void {
|
||||
$business = Business::create(['slug' => strtolower($data['slug']), 'name' => $data['businessName'], 'shortName' => $data['businessName'], 'timezone' => config('app.timezone', 'Europe/London'), 'currency' => 'GBP', 'locale' => 'en-GB']);
|
||||
BusinessDomain::create(['businessId' => $business->id, 'hostname' => strtolower($data['domain'] ?: $request->getHost()), 'isPrimary' => true, 'isActive' => true]);
|
||||
BusinessSettings::create(['businessId' => $business->id]);
|
||||
BookingRule::create(['businessId' => $business->id, 'allowGuestBooking' => true]);
|
||||
$role = Role::create(['businessId' => $business->id, 'name' => 'Owner', 'isSystem' => true]);
|
||||
foreach (Permission::cases() as $permission) RolePermission::create(['roleId' => $role->id, 'permission' => $permission]);
|
||||
$user = User::create(['name' => $data['adminName'], 'email' => strtolower($data['adminEmail']), 'passwordHash' => Hash::make($data['password']), 'emailVerified' => now()]);
|
||||
$staff = StaffProfile::create(['userId' => $user->id, 'businessId' => $business->id, 'roleId' => $role->id, 'status' => 'ACTIVE', 'isBookableOnline' => false]);
|
||||
ApplicationInstallation::create(['id' => 'singleton', 'status' => InstallationStatus::Installed, 'installedAt' => now(), 'installedVersion' => 'laravel-1', 'initialAdminUserId' => $user->id, 'primaryBusinessId' => $business->id, 'metadata' => ['starter' => true]]);
|
||||
});
|
||||
|
||||
Auth::login($admin);
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->route('manager.dashboard')->with('status', 'Platform setup complete. Create your first business from Manager.');
|
||||
}
|
||||
|
||||
private function isInstalled(): bool
|
||||
{
|
||||
try {
|
||||
return ApplicationInstallation::query()->where('status', InstallationStatus::Installed)->exists();
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
return redirect()->route('install')->with('status', 'Installation complete. You can now sign in.');
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\BusinessDomain;
|
||||
use App\Services\Businesses\BusinessProvisioningService;
|
||||
use App\Services\Tenant\DomainNormalizer;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class ManagerBusinessController extends Controller
|
||||
{
|
||||
public function create(): View
|
||||
{
|
||||
return view('manager.businesses.create');
|
||||
}
|
||||
|
||||
public function store(Request $request, BusinessProvisioningService $provisioner, DomainNormalizer $domains): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:160'],
|
||||
'shortName' => ['nullable', 'string', 'max:80'],
|
||||
'slug' => ['required', 'alpha_dash', 'max:80', 'unique:Business,slug'],
|
||||
'domain' => ['required', 'string', 'max:253'],
|
||||
'email' => ['nullable', 'email', 'max:190'],
|
||||
'phone' => ['nullable', 'string', 'max:40'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'currency' => ['required', 'string', 'size:3'],
|
||||
'ownerName' => ['required', 'string', 'max:160'],
|
||||
'ownerEmail' => ['required', 'email', 'max:190', 'unique:User,email'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$data['domain'] = $domains->normalize($data['domain']);
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
return back()->withErrors(['domain' => $exception->getMessage()])->withInput();
|
||||
}
|
||||
|
||||
if (BusinessDomain::query()->where('hostname', $data['domain'])->exists()) {
|
||||
return back()->withErrors(['domain' => 'That hostname is already assigned to another business.'])->withInput();
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $provisioner->provision($data, [
|
||||
'name' => $data['ownerName'],
|
||||
'email' => $data['ownerEmail'],
|
||||
], $request->user());
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
return back()->withErrors(['domain' => $exception->getMessage()])->withInput();
|
||||
}
|
||||
|
||||
$hostname = $result->business->primaryDomain()->value('hostname');
|
||||
$invitationUrl = request()->getScheme().'://'.$hostname.'/invite/'.$result->ownerInvitation->token;
|
||||
|
||||
return redirect()->route('manager.businesses.show', $result->business)
|
||||
->with('status', 'Business created. The owner can finish setup using the invitation link below.')
|
||||
->with('owner_invitation_url', $invitationUrl);
|
||||
}
|
||||
|
||||
public function show(Business $business): View
|
||||
{
|
||||
$business->load(['domains', 'settings', 'bookingRule', 'hours', 'staff.user', 'staff.role']);
|
||||
|
||||
return view('manager.businesses.show', compact('business'));
|
||||
}
|
||||
|
||||
public function edit(Business $business): View
|
||||
{
|
||||
$business->load('primaryDomain');
|
||||
|
||||
return view('manager.businesses.edit', compact('business'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Business $business, DomainNormalizer $domains): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:160'],
|
||||
'shortName' => ['nullable', 'string', 'max:80'],
|
||||
'slug' => ['required', 'alpha_dash', 'max:80', 'unique:Business,slug,'.$business->id.',id'],
|
||||
'domain' => ['required', 'string', 'max:253'],
|
||||
'email' => ['nullable', 'email', 'max:190'],
|
||||
'phone' => ['nullable', 'string', 'max:40'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
'currency' => ['required', 'string', 'size:3'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$hostname = $domains->normalize($data['domain']);
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
return back()->withErrors(['domain' => $exception->getMessage()])->withInput();
|
||||
}
|
||||
|
||||
$domainTaken = BusinessDomain::query()
|
||||
->where('hostname', $hostname)
|
||||
->where('businessId', '!=', $business->id)
|
||||
->exists();
|
||||
|
||||
if ($domainTaken) {
|
||||
return back()->withErrors(['domain' => 'That hostname is already assigned to another business.'])->withInput();
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($business, $data, $hostname): void {
|
||||
$business->update([
|
||||
'name' => trim($data['name']),
|
||||
'shortName' => filled($data['shortName'] ?? null) ? trim($data['shortName']) : null,
|
||||
'slug' => strtolower(trim($data['slug'])),
|
||||
'email' => filled($data['email'] ?? null) ? strtolower(trim($data['email'])) : null,
|
||||
'phone' => filled($data['phone'] ?? null) ? trim($data['phone']) : null,
|
||||
'timezone' => $data['timezone'],
|
||||
'currency' => strtoupper($data['currency']),
|
||||
]);
|
||||
|
||||
$primary = $business->domains()->where('isPrimary', true)->first();
|
||||
if ($primary) {
|
||||
$primary->update(['hostname' => $hostname]);
|
||||
} else {
|
||||
BusinessDomain::create(['businessId' => $business->id, 'hostname' => $hostname, 'isPrimary' => true, 'isActive' => true]);
|
||||
}
|
||||
});
|
||||
|
||||
return redirect()->route('manager.businesses.show', $business)->with('status', 'Business details updated.');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Business;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ManagerDashboardController extends Controller
|
||||
{
|
||||
public function __invoke(): View
|
||||
{
|
||||
$businesses = Business::query()
|
||||
->with(['primaryDomain', 'staff.user', 'staff.role'])
|
||||
->withCount(['staff', 'domains'])
|
||||
->latest('createdAt')
|
||||
->get();
|
||||
|
||||
return view('manager.dashboard', [
|
||||
'businesses' => $businesses,
|
||||
'businessCount' => $businesses->count(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\StaffInvitation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class OwnerInvitationController extends Controller
|
||||
{
|
||||
public function show(string $token): View|RedirectResponse
|
||||
{
|
||||
$invitation = $this->findValidInvitation($token);
|
||||
|
||||
return view('auth.owner-invite', [
|
||||
'invitation' => $invitation,
|
||||
'business' => $invitation->business,
|
||||
]);
|
||||
}
|
||||
|
||||
public function accept(Request $request, string $token): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
]);
|
||||
|
||||
$invitation = $this->findValidInvitation($token);
|
||||
|
||||
DB::transaction(function () use ($invitation, $data): void {
|
||||
$invitation->staff->user->update([
|
||||
'passwordHash' => Hash::make($data['password']),
|
||||
'emailVerified' => now(),
|
||||
]);
|
||||
$invitation->update(['acceptedAt' => now()]);
|
||||
});
|
||||
|
||||
Auth::login($invitation->staff->user->fresh());
|
||||
$request->session()->regenerate();
|
||||
|
||||
$hostname = $invitation->business->primaryDomain?->hostname;
|
||||
$destination = $hostname
|
||||
? request()->getScheme().'://'.$hostname.'/owner'
|
||||
: route('owner.dashboard');
|
||||
|
||||
return redirect()->to($destination)->with('status', 'Your owner workspace is ready.');
|
||||
}
|
||||
|
||||
private function findValidInvitation(string $token): StaffInvitation
|
||||
{
|
||||
$invitation = StaffInvitation::query()->with(['business.primaryDomain', 'staff.user'])->where('token', $token)->firstOrFail();
|
||||
|
||||
abort_if($invitation->acceptedAt || $invitation->expiresAt->isPast(), 410, 'This invitation has expired or has already been used.');
|
||||
|
||||
return $invitation;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\InstallationStatus;
|
||||
use App\Models\ApplicationInstallation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PlatformAuthController extends Controller
|
||||
{
|
||||
public function showLogin(Request $request): View|RedirectResponse
|
||||
{
|
||||
if ($request->user()?->isSuperAdmin) {
|
||||
return redirect()->route('manager.dashboard');
|
||||
}
|
||||
|
||||
if (! ApplicationInstallation::query()->where('status', InstallationStatus::Installed)->exists()) {
|
||||
return redirect()->route('install');
|
||||
}
|
||||
|
||||
return view('auth.platform-sign-in');
|
||||
}
|
||||
|
||||
public function login(Request $request): RedirectResponse
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email', 'max:190'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
|
||||
if (! Auth::attempt($credentials, $request->boolean('remember')) || ! $request->user()?->isSuperAdmin) {
|
||||
Auth::logout();
|
||||
|
||||
return back()->withErrors(['email' => 'Those platform administrator details do not match our records.'])->onlyInput('email');
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('manager.dashboard'));
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureSuperAdmin
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (! $request->user()) {
|
||||
return redirect()->route('manager.login');
|
||||
}
|
||||
|
||||
abort_unless($request->user()->isSuperAdmin === true, 403);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Business;
|
||||
use App\Models\BusinessDomain;
|
||||
use App\Services\Tenant\TenantContext;
|
||||
use App\Services\Tenant\DomainNormalizer;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -14,25 +14,26 @@ class ResolveTenant
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$hostname = null;
|
||||
$hostname = strtolower(rtrim($request->getHost(), '.'));
|
||||
try {
|
||||
$hostname = app(DomainNormalizer::class)->normalize($request->getHost());
|
||||
$business = BusinessDomain::query()
|
||||
->where('hostname', $hostname)
|
||||
->where('isActive', true)
|
||||
->with('business')
|
||||
->first()?->business;
|
||||
|
||||
if (! $business && app()->environment('local')) {
|
||||
$slug = config('app.default_business_slug');
|
||||
$business = $slug
|
||||
? Business::query()->where('slug', $slug)->first()
|
||||
: null;
|
||||
}
|
||||
} catch (Throwable) {
|
||||
// The install page and framework smoke tests must remain
|
||||
// reachable before the tenant schema has been created.
|
||||
$business = null;
|
||||
}
|
||||
|
||||
if (! $business) {
|
||||
app(TenantContext::class)->forget();
|
||||
}
|
||||
|
||||
if ($business) {
|
||||
app(TenantContext::class)->set($business);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user