Initial Ladill Frontdesk release with deploy pipeline.
Visitor management app with SSO, kiosk, badges, reports, and Gitea CI deploy to frontdesk.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Frontdesk\DeviceService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuthenticateFrontdeskDevice
|
||||
{
|
||||
public function __construct(
|
||||
protected DeviceService $devices,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next, string $type = 'kiosk'): Response
|
||||
{
|
||||
$token = $request->route('token') ?? $request->header('X-Device-Token');
|
||||
|
||||
abort_unless(is_string($token) && $token !== '', 404);
|
||||
|
||||
$device = $this->devices->findByToken($token);
|
||||
|
||||
abort_unless($device && $device->type === $type, 404);
|
||||
|
||||
$this->devices->recordHeartbeat($device);
|
||||
|
||||
$request->attributes->set('frontdesk.device', $device->fresh());
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Generic service-to-service auth for internal APIs. Validates the bearer token
|
||||
* against per-consumer keys in config("{namespace}.service_api_keys") with a
|
||||
* constant-time compare, and records the caller. Usage: auth.service:crm.
|
||||
*/
|
||||
class AuthenticateService
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $namespace = 'crm'): Response
|
||||
{
|
||||
$token = (string) $request->bearerToken();
|
||||
$caller = null;
|
||||
|
||||
if ($token !== '') {
|
||||
foreach ((array) config("{$namespace}.service_api_keys", []) as $name => $key) {
|
||||
if (is_string($key) && $key !== '' && hash_equals($key, $token)) {
|
||||
$caller = $name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($caller === null) {
|
||||
return response()->json(['error' => 'Unauthorized.'], 401);
|
||||
}
|
||||
|
||||
$request->attributes->set('service_caller', $caller);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Frontdesk\FrontdeskPermissions;
|
||||
use App\Services\Frontdesk\OrganizationResolver;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureFrontdeskAbility
|
||||
{
|
||||
public function __construct(
|
||||
protected OrganizationResolver $organizations,
|
||||
protected FrontdeskPermissions $permissions,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next, string $ability): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_unless($user, 403);
|
||||
|
||||
$organization = $this->organizations->resolveForUser($user);
|
||||
abort_unless($organization, 403);
|
||||
|
||||
$member = $this->organizations->memberFor($user, $organization);
|
||||
abort_unless($this->permissions->can($member, $ability), 403);
|
||||
|
||||
$request->attributes->set('frontdesk.organization', $organization);
|
||||
$request->attributes->set('frontdesk.member', $member);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Frontdesk\OrganizationResolver;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureOrganizationSetup
|
||||
{
|
||||
public function __construct(
|
||||
protected OrganizationResolver $organizations,
|
||||
) {}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($request->routeIs('frontdesk.onboarding*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (! $this->organizations->isOnboarded($user)) {
|
||||
return redirect()->route('frontdesk.onboarding.show');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* App sessions are subordinate to the platform (auth.ladill.com) session.
|
||||
* When the platform session ends, clear this app's local session too.
|
||||
*/
|
||||
class EnsurePlatformSession
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$authDomain = trim((string) config('app.auth_domain', ''));
|
||||
if ($authDomain === '') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$cookieHeader = (string) $request->headers->get('Cookie', '');
|
||||
if ($cookieHeader === '') {
|
||||
return $this->clearAppSession($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders(['Cookie' => $cookieHeader])
|
||||
->timeout(3)
|
||||
->get('https://'.$authDomain.'/sso/ping');
|
||||
} catch (\Throwable) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if ($response->status() === 401) {
|
||||
return $this->clearAppSession($request);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function clearAppSession(Request $request): Response
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('sso.connect', [
|
||||
'redirect' => $request->fullUrl(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Injects a branded boot splash (Ladill Mail style) into authenticated HTML
|
||||
* pages. The splash shows once per browser session while the app loads, then
|
||||
* fades out — masking the client-side boot so the app feels instant.
|
||||
*/
|
||||
class InjectBootSplash
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
if (! $request->isMethod('GET') || ! Auth::check()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (! str_contains((string) $response->headers->get('Content-Type', ''), 'text/html')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$content = $response->getContent();
|
||||
if (! is_string($content)
|
||||
|| stripos($content, '<body') === false
|
||||
|| str_contains($content, 'id="ladill-boot"')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$splash = View::make('partials.boot-splash')->render();
|
||||
$content = preg_replace_callback('/<body\b[^>]*>/i', fn ($m) => $m[0].$splash, $content, 1);
|
||||
|
||||
if (is_string($content)) {
|
||||
$response->setContent($content);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Shares the signed-in account with views. CRM data is scoped per account by
|
||||
* the user's public_id (owner_ref); today every user acts as their own
|
||||
* account (team support graduates here later, like the platform pattern).
|
||||
*/
|
||||
class SetActingAccount
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($user = $request->user()) {
|
||||
$request->attributes->set('actingAccount', $user);
|
||||
|
||||
if (! $request->is('api/*')) {
|
||||
View::share('actingAccount', $user);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user