Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Providers;
use App\Listeners\SendEmailNewMessage;
use App\Models\Bank;
use App\Models\Call;
use App\Models\Post;
use App\Models\Transaction;
use App\Observers\BankObserver;
use App\Observers\CallObserver;
use App\Observers\PostObserver;
use App\Observers\TransactionObserver;
use App\Services\PresenceService;
use App\View\Components\GuestLayout;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Laravel\Scout\EngineManager;
use Livewire\Livewire;
use Matchish\ScoutElasticSearch\Engines\ElasticSearchEngine;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->bind(
'Matchish\ScoutElasticSearch\ElasticSearch\EloquentHitsIteratorAggregate',
'app\Overrides\Matchish\ScoutElasticSearch\ElasticSearch\EloquentHitsIteratorAggregate'
);
// Register custom Blade directive to conditionally check on activeProfile type (User, Organization, Bank, Admin)
Blade::directive('profile', function ($expression) {
return "<?php if (strtolower(getActiveProfileType()) === strtolower($expression)): ?>";
});
Blade::directive('endprofile', function () {
return "<?php endif; ?>";
});
// Register PresenceService
$this->app->singleton(PresenceService::class);
// Register custom SocialShare service to support custom icons
$this->app->bind(\Enflow\SocialShare\SocialShare::class, \App\Services\CustomSocialShare::class);
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Fix manifest not found error: enforce the correct location of the manifest.json file
Vite::useManifestFilename('.vite/manifest.json');
// Fix 'Specified key was too long error' when storing emoji's as string in DB.
Schema::defaultStringLength(191);
if (!Collection::hasMacro('paginate')) {
Collection::macro(
'paginate',
function ($perPage = 15, $page = null, $options = []) {
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
return (new LengthAwarePaginator(
$this->forPage($page, $perPage),
$this->count(),
$perPage,
$page,
$options
))
->withPath('');
}
);
}
// Manual Elasticsearch driver registration
resolve(EngineManager::class)->extend('matchish-elasticsearch', function ($app) {
return resolve(ElasticSearchEngine::class);
});
// Register Guest Layout
Blade::component('guest-layout', GuestLayout::class);
// Register lay-outs depending ob authorization
Blade::directive('layout', function () {
return Auth::check() ? 'app-layout' : 'guest-layout';
});
Blade::if('usercan', function ($permission) {
// Get the web user (all permissions are assigned to Users, not to Organizations/Banks/Admins)
$webUser = Auth::guard('web')->user();
if (!$webUser) {
return false;
}
// Use request-level cache to prevent recursion during view rendering
static $permissionCache = [];
$cacheKey = $webUser->id . '_' . $permission;
if (isset($permissionCache[$cacheKey])) {
return $permissionCache[$cacheKey];
}
// Permissions are always checked on the web user (guard 'web')
// Organizations/Banks/Admins don't have their own permissions
try {
// Use can() method instead of hasPermissionTo to avoid loading all permissions
$result = $webUser->can($permission);
$permissionCache[$cacheKey] = $result;
return $result;
} catch (\Exception $e) {
// Any error - return false and cache it
$permissionCache[$cacheKey] = false;
return false;
}
});
// Override the vendor Wirechat components with custom extended ones
Livewire::component('wirechat.chat', \App\Http\Livewire\WireChat\Chat\Chat::class);
Livewire::component('wirechat.chats', \App\Http\Livewire\WireChat\Chats\Chats::class);
Livewire::component('wirechat.new.chat', \App\Http\Livewire\WireChat\New\Chat::class);
// Register WireChat event listener
Event::listen(
\Namu\WireChat\Events\MessageCreated::class,
SendEmailNewMessage::class,
);
Livewire::component('wire-chat.typing-indicator', \App\Http\Livewire\WireChat\TypingIndicator::class);
// Register Bank observer to auto-register new banks as love reacters/reactables
Bank::observe(BankObserver::class);
// Register Post observer to auto-register new posts as love reactables
Post::observe(PostObserver::class);
// Register Call observer to auto-register new calls as love reactables
Call::observe(CallObserver::class);
// Pause calls when spending profile runs out of credits after a transaction
Transaction::observe(TransactionObserver::class);
// Register Conversation observer to auto-set disappearing_started_at
\Namu\WireChat\Models\Conversation::observe(\App\Observers\ConversationObserver::class);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Providers;
use App\Auth\DockerSessionGuard;
use App\Models\Bank;
use App\Policies\BankPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
// Set remember me duration from platform configuration
$rememberMeDays = timebank_config('auth.remember_me_days', 90);
$rememberMeMinutes = $rememberMeDays * 24 * 60; // Convert days to minutes
// Use custom guard in Docker that doesn't migrate sessions
if (env('IS_DOCKER', false)) {
Auth::extend('session', function ($app, $name, array $config) use ($rememberMeMinutes) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new DockerSessionGuard($name, $provider, $app['session.store']);
// Set the cookie jar on the guard
$guard->setCookieJar($app['cookie']);
// If a request is available, set it on the guard
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($app->refresh('request', $guard, 'setRequest'));
}
// Set remember me duration
$guard->setRememberDuration($rememberMeMinutes);
return $guard;
});
}
// Set remember me duration for all standard guards
foreach (['web', 'organization', 'bank', 'admin'] as $guardName) {
$guard = Auth::guard($guardName);
if (method_exists($guard, 'setRememberDuration')) {
$guard->setRememberDuration($rememberMeMinutes);
}
}
// Spatie Laravel-Permissions:
// Implicitly grant "Super-Admin" role all permission checks using can()
Gate::before(function ($user, $ability) {
if ($user->hasRole('Super-Admin')) {
return true;
}
});
// Add these explicit gate definitions:
// These gates check permissions directly via Spatie's permission system
Gate::define('manage banks', function ($user) {
try {
// Always check on web guard since that's where permissions are stored
if ($user instanceof \App\Models\User) {
return $user->hasPermissionTo('manage banks', 'web');
}
return false;
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
return false;
} catch (\Exception $e) {
return false;
}
});
Gate::define('manage organizations', function ($user) {
try {
// Always check on web guard since that's where permissions are stored
if ($user instanceof \App\Models\User) {
return $user->hasPermissionTo('manage organizations', 'web');
}
return false;
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
return false;
} catch (\Exception $e) {
return false;
}
});
Gate::define('manage admins', function ($user) {
try {
// Always check on web guard since that's where permissions are stored
if ($user instanceof \App\Models\User) {
return $user->hasPermissionTo('manage admins', 'web');
}
return false;
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
return false;
} catch (\Exception $e) {
return false;
}
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Broadcasting routes are now registered in routes/web.php within the localized route group
require base_path('routes/channels.php');
Broadcast::channel('switch-profile', function ($user) {
return $user;
});
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Providers;
use App\Events\Auth\RegisteredByAdmin;
use App\Events\ProfileSwitchEvent;
use App\Events\ProfileVerified;
use App\Listeners\Auth\SendAdminCreatedVerificationNotification;
use App\Listeners\LogProfileSwitch;
use App\Listeners\LoginSuccessful;
use App\Listeners\ReactionCreatedListener;
use App\Listeners\ReactionRemovedListener;
use App\Listeners\SendEmailNewMessage;
use App\Listeners\UpdateProfileEmailVerifiedAt;
use Cog\Laravel\Love\Reaction\Events\ReactionHasBeenAdded;
use Cog\Laravel\Love\Reaction\Events\ReactionHasBeenRemoved;
use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
RegisteredByAdmin::class => [
SendAdminCreatedVerificationNotification::class,
],
Login::class => [
LoginSuccessful::class
],
ReactionHasBeenAdded::class => [
ReactionCreatedListener::class
],
ReactionHasBeenRemoved::class => [
ReactionRemovedListener::class
],
'Illuminate\Auth\Events\Logout' => [
'App\Listeners\HandleUserLogout',
],
// Log the date and ip of the profile that is switched to:
// When the ProfileSwitchEvent triggers the LogProfileSwitch listens
ProfileSwitchEvent::class => [
LogProfileSwitch::class,
],
// Email verification of profile models (organization's, banks, admins etc)
ProfileVerified::class => [
UpdateProfileEmailVerifiedAt::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
//
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Http\Controllers\VerifyEmailController;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Disable Fortify route registration, a selection of the initial fortify routes are moved ot /routes/fortify.php.
// Custom routes with the same name however are registered in web.php. This is done because Fortify does not allow to register routes with the same name.
Fortify::ignoreRoutes();
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::verifyEmailView(fn () => view('auth.verify-email'));
RateLimiter::for('login', function (Request $request) {
$loginIdentifier = (string) $request->input('name'); // Changed from $request->email to $request->input('name')
return Limit::perMinute(10)->by($loginIdentifier.$request->ip());
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
// Rehash Cyclos salted sha256 passwords on first login
Fortify::authenticateUsing(function (Request $request) {
// Attempt to find the user by email or name.
$user = User::where('email', $request->name)
->orWhere('name', $request->name)
->first();
if (!$user) {
info('Auth failed: User not identified with login identifier: ' . $request->auth);
return null;
}
if ($user->isRemoved()) {
info('Auth failed: User ' . $user->name . ' is marked as removed (soft-deleted).');
return null;
}
// If maintenance mode is enabled, check if user has admin relationship
if (isMaintenanceMode()) {
$hasAdminRelation = $user->admins()->exists();
if (!$hasAdminRelation) {
info('Auth failed: Maintenance mode is active and user ' . $user->name . ' does not have admin relationship.');
session()->flash('maintenance_mode_active', true);
throw ValidationException::withMessages([
'name' => __('Login is currently disabled due to site maintenance. Only administrator accounts can access the site at this time. Please try again later.'),
]);
}
info('Auth allowed during maintenance: User ' . $user->name . ' has admin relationship.');
}
info('User identified: ' . $user->name . ' (ID: ' . $user->id . ') attempting login with identifier: ' . $request->auth);
$passwordVerified = false;
// Check for Cyclos password first if salt exists
if (!empty($user->cyclos_salt)) {
info('Attempting Cyclos password verification for user: ' . $user->name);
$concatenated = $user->cyclos_salt . $request->password;
$hashedInputPassword = hash("sha256", $concatenated);
if (strtolower($hashedInputPassword) === strtolower($user->password)) {
info('Cyclos password verified for user: ' . $user->name);
// Prepare user model for update: rehash password and clear salt
$user->password = Hash::make($request->password);
$user->cyclos_salt = null;
$passwordVerified = true;
info('Cyclos password prepared for rehashing for user: ' . $user->name);
} else {
info('Cyclos password verification failed for user: ' . $user->name . '. Will attempt standard Laravel hash check.');
}
}
// If not verified by Cyclos, or if cyclos_salt was empty, try Laravel's default hash check
if (!$passwordVerified && Hash::check($request->password, $user->password)) {
info('Laravel password verified for user: ' . $user->name);
$passwordVerified = true;
}
if ($passwordVerified) {
info('Auth success for user: ' . $user->name);
// Update last login details
$now = Carbon::now()->toDateTimeString();
$user->last_login_at = $now;
$user->last_login_ip = $request->getClientIp();
if (timebank_config('profile_inactive.re-activate_at_login')) {
if (!$user->isActive()) {
$user->inactive_at = null;
$activated = true;
}
}
// Save all changes (rehashed password, cleared salt, login details)
$user->save();
if (isset($activated) && $activated) {
info('User re-activated: ' . $user->name);
}
return $user;
}
info('Auth failed: Password mismatch for user: ' . $user->name . ' with identifier: ' . $request->auth);
return null;
});
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Providers;
use App\Actions\Jetstream\DeleteUser;
use App\Http\Livewire\ProfileUser\UpdateProfilePersonalForm;
use App\Http\Livewire\Profile\DeleteUserForm;
use App\Http\Livewire\Profile\TwoFactorAuthenticationForm;
use App\Http\Livewire\Profile\UpdatePasswordForm;
use App\Http\Livewire\Profile\UpdateProfilePhoneForm;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;
use Laravel\Jetstream\Jetstream;
use Livewire\Livewire;
class JetstreamServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->registerComponent('toaster');
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Jetstream::ignoreRoutes(); // Completely disable Jetstream routes, stops Jetstream-specific routes (teams, profile, API tokens, etc.).
$this->configurePermissions();
Jetstream::deleteUsersUsing(DeleteUser::class);
//TODO: fix this registration. Why is it registered here and like this?
Livewire::component('profile-user.update-profile-personal-form', UpdateProfilePersonalForm::class);
Livewire::component('profile-user.update-profile-phone-form', UpdateProfilePhoneForm::class);
// Register customized Jetstream DeleteUserForm
Livewire::component('profile.delete-user-form', DeleteUserForm::class);
// CRITICAL SECURITY: Register custom TwoFactorAuthenticationForm with authorization checks
// This overrides the vendor Jetstream component to prevent IDOR attacks
Livewire::component('profile.two-factor-authentication-form', TwoFactorAuthenticationForm::class);
// CRITICAL SECURITY: Register custom UpdatePasswordForm with authorization checks
// This overrides the vendor Jetstream component to prevent unauthorized password changes
Livewire::component('profile.update-password-form', UpdatePasswordForm::class);
// Register LoginResponse for conditional redirects in Http/Responses/LoginResponse.php
// This is used to load language preference after login
$this->app->singleton(
\Laravel\Fortify\Contracts\LoginResponse::class,
\App\Http\Responses\LoginResponse::class
);
// Register TwofactorLoginResponse for conditional redirects in Http/Responses/LoginResponse.php
// This is used to load language preference after login
$this->app->singleton(
\Laravel\Fortify\Contracts\TwoFactorLoginResponse::class,
\App\Http\Responses\LoginResponse::class
);
}
/**
* Configure the permissions that are available within the application.
*
* @return void
*/
protected function configurePermissions()
{
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::permissions([
'create',
'read',
'update',
'delete',
]);
}
protected function registerComponent(string $component)
{
Blade::component('components.' . $component, $component);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use App\Mail\MailpitCopyListener;
use App\Mail\UniversalBounceHandler;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class MailBounceServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(UniversalBounceHandler::class);
}
/**
* Bootstrap services.
*/
public function boot(): void
{
// Copy to Mailpit first (before bounce handler which may return false and halt propagation)
Event::listen(MessageSending::class, [MailpitCopyListener::class, 'handle']);
// Register the universal bounce handler for all outgoing emails
Event::listen(MessageSending::class, [UniversalBounceHandler::class, 'handle']);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
use \Mcamara\LaravelLocalization\Traits\LoadsTranslatedCachedRoutes;
/**
* The path to the "home" route for your application.
*
* This is used by Laravel authentication to redirect users after login.
*
* @var string
*/
public const HOME = '/main-page'; // Not used because of route localization
/**
* Get the localized home route.
*
* @return string
*/
public static function localizedHome()
{
return \Mcamara\LaravelLocalization\Facades\LaravelLocalization::localizeURL(self::HOME);
}
/**
* The controller namespace for the application.
*
* When present, controller route declarations will automatically be prefixed with this namespace.
*
* @var string|null
*/
// protected $namespace = 'App\\Http\\Controllers';
/**
* Define your route model bindings, pattern filters, etc.
*
* @return void
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(function () {
require base_path('routes/web.php');
require base_path('routes/fortify.php'); // <-- Fortify routes, with some routes disabled as they are customized and therefore in web.php
require base_path('routes/test.php');
});
});
// Route parameter constraints:
Route::pattern('id', '[0-9]+');
Broadcast::routes([
'middleware' => ['auth.any:admin,bank,organization,web'],
]);
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
});
}
}

View File

@@ -0,0 +1,106 @@
<?php
// Create this file: app/Providers/ScoutObserverServiceProvider.php
namespace App\Providers;
use App\Models\Bank;
use App\Models\Organization;
use App\Models\Post;
use App\Models\PostTranslation;
use App\Models\User;
use App\Observers\ScoutReindexObserver;
use Elastic\Elasticsearch\ClientBuilder;
use Illuminate\Support\ServiceProvider;
class ScoutObserverServiceProvider extends ServiceProvider
{
/**
* Register observers for Scout models
*/
public function boot()
{
// Create a single observer instance to share across models
$scoutObserver = new ScoutReindexObserver();
// Register observer for User model
User::observe($scoutObserver);
// Register observer for Organization model
Organization::observe($scoutObserver);
// Register observer for Bank model
Bank::observe($scoutObserver);
// Register observer for Post model
Post::observe($scoutObserver);
// TODO NEXT: why not re-indexing with frsh data?
// Add PostTranslation observer with custom logic
PostTranslation::saved(function (PostTranslation $translation) {
if ($translation->post_id) {
$post = Post::find($translation->post_id);
\Log::info('Before searchable:', [
'post_id' => $post->id,
'translations_loaded' => $post->relationLoaded('translations'),
'translation_count' => $post->translations->count(),
'searchable_array' => $post->toSearchableArray()
]);
$post->searchable();
// Wait a moment for Elasticsearch
sleep(1);
// Correct way to get Elasticsearch client for Matchish package
$hosts = config('elastic.client.default.hosts', ['localhost:9200']);
// Ensure hosts is always an array
if (!is_array($hosts)) {
$hosts = [$hosts];
}
$client = ClientBuilder::create()
->setHosts($hosts)
->build();
try {
$exists = $client->exists([
'index' => $post->searchableAs(),
'id' => $post->getScoutKey()
]);
$indexedData = null;
if ($exists) {
$response = $client->get([
'index' => $post->searchableAs(),
'id' => $post->getScoutKey()
]);
$indexedData = $response['_source'] ?? null;
}
\Log::info('After searchable:', [
'exists_in_index' => $exists,
'index_name' => $post->searchableAs(),
'scout_key' => $post->getScoutKey(),
'indexed_till_en' => $indexedData['post_translations']['till_en'] ?? 'not found'
]);
} catch (\Exception $e) {
\Log::error('Elasticsearch check failed', [
'error' => $e->getMessage()
]);
}
}
});
PostTranslation::deleted(function (PostTranslation $translation) {
if ($translation->post) {
$translation->post->unsetRelation('translations');
$translation->post->searchable();
}
});
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
class ThemeServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
// Register @theme Blade directive
Blade::directive('theme', function ($expression) {
return "<?php echo theme({$expression}); ?>";
});
// Register @themeId Blade directive
Blade::directive('themeId', function () {
return "<?php echo theme_id(); ?>";
});
// Register @themeName Blade directive
Blade::directive('themeName', function () {
return "<?php echo theme_name(); ?>";
});
// Register @themeColor Blade directive
Blade::directive('themeColor', function ($expression) {
return "<?php echo theme_color({$expression}); ?>";
});
// Register @themeFont Blade directive
Blade::directive('themeFont', function ($expression) {
return "<?php echo theme_font({$expression}); ?>";
});
// Register @isTheme Blade directive
Blade::directive('isTheme', function ($expression) {
return "<?php if (is_theme({$expression})): ?>";
});
// Register @endIsTheme Blade directive
Blade::directive('endIsTheme', function () {
return "<?php endif; ?>";
});
// Register @themeCssVars Blade directive
Blade::directive('themeCssVars', function () {
return "<?php echo theme_css_vars(); ?>";
});
}
}