Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1de78bdce3 | |||
| f78d97dae5 | |||
| c80891b25e |
12 changed files with 247 additions and 14 deletions
28
app/Console/Commands/PurgeDemoAccountsCommand.php
Normal file
28
app/Console/Commands/PurgeDemoAccountsCommand.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Planner;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class PurgeDemoAccountsCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'demo:purge';
|
||||||
|
|
||||||
|
protected $description = 'Purge demo accounts older than 24 hours';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (! is_mode_demo()) {
|
||||||
|
$this->error('This command can only run in demo mode.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = Planner::where('created_at', '<', now()->subHours(24))->delete();
|
||||||
|
|
||||||
|
$this->info("Purged {$count} demo accounts.");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ enum AppModeEnum: string
|
||||||
{
|
{
|
||||||
case APP = 'app';
|
case APP = 'app';
|
||||||
case SAAS = 'saas';
|
case SAAS = 'saas';
|
||||||
|
case DEMO = 'demo';
|
||||||
|
|
||||||
public static function current(): self
|
public static function current(): self
|
||||||
{
|
{
|
||||||
|
|
@ -21,4 +22,19 @@ public function isSaas(): bool
|
||||||
{
|
{
|
||||||
return $this === self::SAAS;
|
return $this === self::SAAS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isDemo(): bool
|
||||||
|
{
|
||||||
|
return $this === self::DEMO;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiresSubscription(): bool
|
||||||
|
{
|
||||||
|
return $this === self::SAAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function allowsLogout(): bool
|
||||||
|
{
|
||||||
|
return $this !== self::DEMO;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +34,10 @@ public function login(Request $request)
|
||||||
|
|
||||||
public function logout(Request $request)
|
public function logout(Request $request)
|
||||||
{
|
{
|
||||||
|
if (is_mode_demo()) {
|
||||||
|
return redirect()->route('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
|
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
|
|
|
||||||
43
app/Http/Middleware/DemoMiddleware.php
Normal file
43
app/Http/Middleware/DemoMiddleware.php
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Models\Planner;
|
||||||
|
use Closure;
|
||||||
|
use DishPlanner\Planner\Actions\SeedDemoPlannerAction;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class DemoMiddleware
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
if (! is_mode_demo()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Auth::check()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$planner = DB::transaction(function () {
|
||||||
|
$planner = Planner::create([
|
||||||
|
'name' => 'Demo User',
|
||||||
|
'email' => 'demo-' . Str::uuid() . '@demo.local',
|
||||||
|
'password' => Hash::make(Str::random(32)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
resolve(SeedDemoPlannerAction::class)->execute($planner);
|
||||||
|
|
||||||
|
return $planner;
|
||||||
|
});
|
||||||
|
|
||||||
|
Auth::login($planner);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Enums\AppModeEnum;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
@ -10,7 +11,7 @@ class RequireSubscription
|
||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
if (is_mode_app()) {
|
if (! AppModeEnum::current()->requiresSubscription()) {
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
* @property int $id
|
* @property int $id
|
||||||
* @property static PlannerFactory factory($count = null, $state = [])
|
* @property static PlannerFactory factory($count = null, $state = [])
|
||||||
* @method static first()
|
* @method static first()
|
||||||
|
* @method static create(array $array)
|
||||||
*/
|
*/
|
||||||
class Planner extends Authenticatable
|
class Planner extends Authenticatable
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,3 +15,17 @@ function is_mode_saas(): bool
|
||||||
return AppModeEnum::current()->isSaas();
|
return AppModeEnum::current()->isSaas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (! function_exists('is_mode_demo')) {
|
||||||
|
function is_mode_demo(): bool
|
||||||
|
{
|
||||||
|
return AppModeEnum::current()->isDemo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('allows_logout')) {
|
||||||
|
function allows_logout(): bool
|
||||||
|
{
|
||||||
|
return AppModeEnum::current()->allowsLogout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\DemoMiddleware;
|
||||||
use App\Http\Middleware\ForceJsonResponse;
|
use App\Http\Middleware\ForceJsonResponse;
|
||||||
use App\Http\Middleware\RequireSaasMode;
|
use App\Http\Middleware\RequireSaasMode;
|
||||||
use App\Http\Middleware\RequireSubscription;
|
use App\Http\Middleware\RequireSubscription;
|
||||||
|
|
@ -27,7 +28,7 @@
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
// Apply ForceJsonResponse only to API routes
|
// Apply ForceJsonResponse only to API routes
|
||||||
$middleware->api(ForceJsonResponse::class);
|
$middleware->api(ForceJsonResponse::class);
|
||||||
|
$middleware->web(DemoMiddleware::class);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'subscription' => RequireSubscription::class,
|
'subscription' => RequireSubscription::class,
|
||||||
'saas' => RequireSaasMode::class,
|
'saas' => RequireSaasMode::class,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'mode' => env('APP_MODE', 'app'),
|
'mode' => env('APP_MODE', 'app'),
|
||||||
|
'demo_subscribe_url' => env('APP_DEMO_SUBSCRIBE_URL', 'https://dishplanner.app'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,16 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased bg-gray-600 text-gray-100" x-data="{ mobileMenuOpen: false }">
|
<body class="font-sans antialiased bg-gray-600 text-gray-100" x-data="{ mobileMenuOpen: false }">
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
|
@if(is_mode_demo())
|
||||||
|
<!-- Demo Banner -->
|
||||||
|
<div class="bg-primary text-white text-center py-2 px-4 text-sm sticky top-0 z-[60]">
|
||||||
|
DEMO MODE.
|
||||||
|
<a href="{{ config('app.demo_subscribe_url') }}" target="_blank" rel="noopener noreferrer" class="underline font-semibold hover:text-gray-200 ml-1">
|
||||||
|
Subscribe for only €1 / month →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="border-b-2 border-secondary shadow-sm z-50 mb-8 bg-gray-700">
|
<nav class="border-b-2 border-secondary shadow-sm z-50 mb-8 bg-gray-700">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
|
@ -70,12 +80,14 @@ class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-
|
||||||
Billing
|
Billing
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
|
@if(allows_logout())
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-600 hover:text-accent-blue">
|
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-100 hover:bg-gray-600 hover:text-accent-blue">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -150,12 +162,14 @@ class="block text-2xl font-medium {{ request()->routeIs('schedule.*') ? 'text-ac
|
||||||
Billing
|
Billing
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
|
@if(allows_logout())
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="text-xl text-danger">
|
<button type="submit" class="text-xl text-danger">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="space-y-6 text-center">
|
<div class="space-y-6 text-center">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Artisan::command('inspire', function () {
|
Artisan::command('inspire', function () {
|
||||||
$this->comment(Inspiring::quote());
|
$this->comment(Inspiring::quote());
|
||||||
})->purpose('Display an inspiring quote')->hourly();
|
})->purpose('Display an inspiring quote')->hourly();
|
||||||
|
|
||||||
|
Schedule::command('demo:purge')
|
||||||
|
->dailyAt('03:00')
|
||||||
|
->when(fn () => is_mode_demo());
|
||||||
|
|
|
||||||
105
src/DishPlanner/Planner/Actions/SeedDemoPlannerAction.php
Normal file
105
src/DishPlanner/Planner/Actions/SeedDemoPlannerAction.php
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DishPlanner\Planner\Actions;
|
||||||
|
|
||||||
|
use App\Models\Dish;
|
||||||
|
use App\Models\Planner;
|
||||||
|
use App\Models\User;
|
||||||
|
use DishPlanner\Schedule\Actions\GenerateScheduleForPeriodAction;
|
||||||
|
|
||||||
|
class SeedDemoPlannerAction
|
||||||
|
{
|
||||||
|
private array $dishNames = [
|
||||||
|
'Spaghetti Bolognese',
|
||||||
|
'Chicken Curry',
|
||||||
|
'Caesar Salad',
|
||||||
|
'Beef Stir Fry',
|
||||||
|
'Vegetable Lasagna',
|
||||||
|
'Fish Tacos',
|
||||||
|
'Mushroom Risotto',
|
||||||
|
'BBQ Ribs',
|
||||||
|
'Greek Salad',
|
||||||
|
'Pad Thai',
|
||||||
|
'Margherita Pizza',
|
||||||
|
'Beef Burger',
|
||||||
|
'Chicken Fajitas',
|
||||||
|
'Vegetable Soup',
|
||||||
|
'Salmon Teriyaki',
|
||||||
|
'Lamb Chops',
|
||||||
|
'Shrimp Scampi',
|
||||||
|
'Pulled Pork Sandwich',
|
||||||
|
'Caprese Salad',
|
||||||
|
'Beef Tacos',
|
||||||
|
'Chicken Alfredo',
|
||||||
|
'Vegetable Curry',
|
||||||
|
'Pork Schnitzel',
|
||||||
|
'Tuna Poke Bowl',
|
||||||
|
'Beef Stroganoff',
|
||||||
|
'Chicken Parmesan',
|
||||||
|
'Ratatouille',
|
||||||
|
'Fish and Chips',
|
||||||
|
'Lamb Kebabs',
|
||||||
|
'Shrimp Fried Rice',
|
||||||
|
'Pork Tenderloin',
|
||||||
|
'Nicoise Salad',
|
||||||
|
'Beef Burritos',
|
||||||
|
'Chicken Tikka Masala',
|
||||||
|
'Eggplant Parmesan',
|
||||||
|
'Grilled Salmon',
|
||||||
|
'Lamb Tagine',
|
||||||
|
'Lobster Roll',
|
||||||
|
'Pork Belly',
|
||||||
|
'Waldorf Salad',
|
||||||
|
'Beef Wellington',
|
||||||
|
'Chicken Satay',
|
||||||
|
'Stuffed Peppers',
|
||||||
|
'Miso Glazed Cod',
|
||||||
|
'Lamb Shanks',
|
||||||
|
'Crab Cakes',
|
||||||
|
'Pork Carnitas',
|
||||||
|
'Cobb Salad',
|
||||||
|
'Beef Enchiladas',
|
||||||
|
'Chicken Shawarma',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function execute(Planner $planner): void
|
||||||
|
{
|
||||||
|
$users = $this->createUsers($planner);
|
||||||
|
$this->createDishes($planner, $users);
|
||||||
|
$this->generateSchedule($planner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createUsers(Planner $planner): array
|
||||||
|
{
|
||||||
|
$names = ['Alice', 'Bob', 'Charlie'];
|
||||||
|
|
||||||
|
return array_map(
|
||||||
|
fn (string $name) => User::create([
|
||||||
|
'planner_id' => $planner->id,
|
||||||
|
'name' => $name,
|
||||||
|
]),
|
||||||
|
$names
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createDishes(Planner $planner, array $users): void
|
||||||
|
{
|
||||||
|
foreach ($this->dishNames as $dishName) {
|
||||||
|
$dish = Dish::create([
|
||||||
|
'planner_id' => $planner->id,
|
||||||
|
'name' => $dishName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Randomly assign dish to 1-3 users
|
||||||
|
$count = rand(1, count($users));
|
||||||
|
$assignedUsers = collect($users)->random($count);
|
||||||
|
$userIds = $count === 1 ? [$assignedUsers->id] : $assignedUsers->pluck('id');
|
||||||
|
$dish->users()->attach($userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateSchedule(Planner $planner): void
|
||||||
|
{
|
||||||
|
resolve(GenerateScheduleForPeriodAction::class)->execute($planner);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue