Init
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Extensions;
|
||||
|
||||
use Filament\Forms\Form;
|
||||
use Lunar\Admin\Support\Extending\ResourceExtension;
|
||||
|
||||
class StaffResourceExtension extends ResourceExtension
|
||||
{
|
||||
public function extendForm(Form $form): Form
|
||||
{
|
||||
$schema = collect($form->getComponents())
|
||||
->reject(fn ($component) => method_exists($component, 'getName') && $component->getName() == 'password')
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $form->schema($schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Filament\Pages;
|
||||
|
||||
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Pages\SimplePage;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Modules\Core\Auth\Services\OtpService;
|
||||
|
||||
class Login extends SimplePage
|
||||
{
|
||||
use WithRateLimiting;
|
||||
|
||||
protected static string $view = 'core::auth.filament.pages.login';
|
||||
|
||||
public ?string $email = '';
|
||||
public ?string $otp = '';
|
||||
public bool $otpSent = false;
|
||||
|
||||
private OtpService $otpService;
|
||||
|
||||
public function boot(OtpService $otpService): void
|
||||
{
|
||||
$this->otpService = $otpService;
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (Filament::auth()->check()) {
|
||||
redirect()->intended(Filament::getUrl());
|
||||
}
|
||||
}
|
||||
|
||||
public function requestOtp(): void
|
||||
{
|
||||
$this->validate(['email' => 'required|email']);
|
||||
|
||||
try {
|
||||
$this->rateLimit(5);
|
||||
} catch (TooManyRequestsException) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => 'Too many attempts. Please wait before trying again.',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->otpService->generateAndSend($this->email);
|
||||
|
||||
$this->otpSent = true;
|
||||
}
|
||||
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->validate(['otp' => 'required']);
|
||||
|
||||
try {
|
||||
$this->rateLimit(5);
|
||||
} catch (TooManyRequestsException) {
|
||||
throw ValidationException::withMessages([
|
||||
'otp' => 'Too many attempts. Please wait before trying again.',
|
||||
]);
|
||||
}
|
||||
|
||||
$staff = $this->otpService->validate($this->email, $this->otp);
|
||||
|
||||
if (!$staff) {
|
||||
throw ValidationException::withMessages([
|
||||
'otp' => 'Invalid or expired code.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($staff instanceof FilamentUser && !$staff->canAccessPanel(Filament::getCurrentPanel())) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => 'You do not have access to this panel.',
|
||||
]);
|
||||
}
|
||||
|
||||
Filament::auth()->login($staff);
|
||||
|
||||
session()->regenerate();
|
||||
|
||||
redirect()->intended(Filament::getUrl());
|
||||
}
|
||||
|
||||
public function getTitle(): string | Htmlable
|
||||
{
|
||||
return 'Sign in';
|
||||
}
|
||||
|
||||
public function getHeading(): string | Htmlable
|
||||
{
|
||||
return 'Sign in to your account';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Mail;
|
||||
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
|
||||
class CustomerOtpMail extends Mailable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly string $code,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(subject: 'Ο κωδικός σύνδεσης σου');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(view: 'core::auth.mail.customer-otp');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Mail;
|
||||
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
|
||||
class InviteMail extends Mailable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(subject: 'You have been invited');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(view: 'core::auth.mail.invite');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Mail;
|
||||
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
|
||||
class OtpMail extends Mailable
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $name,
|
||||
public readonly string $code,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(subject: 'Your login code');
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(view: 'core::auth.mail.otp');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Models;
|
||||
|
||||
use Lunar\Admin\Models\Staff as ModelsStaff;
|
||||
|
||||
class Staff extends ModelsStaff
|
||||
{
|
||||
protected $fillable = [
|
||||
'first_name',
|
||||
'last_name',
|
||||
'admin',
|
||||
'email',
|
||||
'otp_code',
|
||||
'otp_expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'admin' => 'bool',
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'otp_expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Lunar\Models\Contracts\Customer;
|
||||
use Lunar\Facades\ModelManifest;
|
||||
use Modules\Core\Auth\Mail\CustomerOtpMail;
|
||||
|
||||
class CustomerOtpService
|
||||
{
|
||||
private const EXPIRY_MINUTES = 10;
|
||||
private const CODE_LENGTH = 6;
|
||||
|
||||
public function generateAndSend(string $email): bool
|
||||
{
|
||||
$customer = $this->findByEmail($email);
|
||||
|
||||
if (! $customer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = str_pad((string) random_int(0, 999999), self::CODE_LENGTH, '0', STR_PAD_LEFT);
|
||||
|
||||
$customer->otp_code = $code;
|
||||
$customer->otp_expires_at = now()->addMinutes(self::EXPIRY_MINUTES);
|
||||
$customer->save();
|
||||
|
||||
Mail::to($email)->send(new CustomerOtpMail($customer->full_name, $code));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validate(string $email, string $code): ?Customer
|
||||
{
|
||||
$customer = $this->findByEmail($email);
|
||||
|
||||
if (! $customer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $customer->otp_expires_at || $customer->otp_code != $code || now()->isAfter($customer->otp_expires_at)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customer->otp_code = null;
|
||||
$customer->otp_expires_at = null;
|
||||
$customer->save();
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
private function findByEmail(string $email): ?Customer
|
||||
{
|
||||
$model = ModelManifest::get(Customer::class);
|
||||
|
||||
return $model::query()
|
||||
->whereHas('emails', fn ($query) => $query->where('email', $email))
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Modules\Core\Auth\Mail\OtpMail;
|
||||
use Modules\Core\Auth\Models\Staff;
|
||||
|
||||
class OtpService
|
||||
{
|
||||
private const EXPIRY_MINUTES = 10;
|
||||
private const CODE_LENGTH = 6;
|
||||
|
||||
public function generateAndSend(string $email): bool
|
||||
{
|
||||
$staff = Staff::where('email', $email)->first();
|
||||
|
||||
if (! $staff) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = str_pad((string) random_int(0, 999999), self::CODE_LENGTH, '0', STR_PAD_LEFT);
|
||||
|
||||
$staff->otp_code = $code;
|
||||
$staff->otp_expires_at = now()->addMinutes(self::EXPIRY_MINUTES);
|
||||
$staff->save();
|
||||
|
||||
Mail::to($staff->email)->send(new OtpMail($staff->first_name, $code));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validate(string $email, string $code): ?Staff
|
||||
{
|
||||
$staff = Staff::where('email', $email)->first();
|
||||
|
||||
if (! $staff) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $staff->otp_expires_at || $staff->otp_code != $code || now()->isAfter($staff->otp_expires_at)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$staff->otp_code = null;
|
||||
$staff->otp_expires_at = null;
|
||||
$staff->save();
|
||||
|
||||
return $staff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AnonymizeCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:anonymize";
|
||||
|
||||
protected $description = "Anonymize personal data in users and lunar_customers";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!app()->environment("local")) {
|
||||
$this->error(
|
||||
"This command can only be run in the local environment.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->confirm(
|
||||
"This will permanently overwrite personal data. Continue?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table("users")
|
||||
->get()
|
||||
->each(function ($user) {
|
||||
DB::table("users")
|
||||
->where("id", $user->id)
|
||||
->update([
|
||||
"name" => "User {$user->id}",
|
||||
"email" => "user_{$user->id}@example.com",
|
||||
"password" => Hash::make("password"),
|
||||
"remember_token" => null,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->info("Users anonymized.");
|
||||
|
||||
DB::table("lunar_customers")
|
||||
->get()
|
||||
->each(function ($customer) {
|
||||
DB::table("lunar_customers")
|
||||
->where("id", $customer->id)
|
||||
->update([
|
||||
"title" => null,
|
||||
"first_name" => "Customer",
|
||||
"last_name" => (string) $customer->id,
|
||||
"company_name" => null,
|
||||
"tax_identifier" => null,
|
||||
"account_ref" => null,
|
||||
"meta" => null,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->info("Customers anonymized.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Lunar\Admin\Console\Commands\MakeLunarAdminCommand;
|
||||
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
class CreateAdminCommand extends MakeLunarAdminCommand
|
||||
{
|
||||
protected $signature = 'lunar:create-admin
|
||||
{--firstname= : The first name of the user}
|
||||
{--lastname= : The last name of the user}
|
||||
{--email= : A valid and unique email address}';
|
||||
|
||||
protected function getUserData(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => $this->options['firstname'] ?? text(
|
||||
label: 'First Name',
|
||||
required: true,
|
||||
),
|
||||
|
||||
'last_name' => $this->options['lastname'] ?? text(
|
||||
label: 'Last Name',
|
||||
required: true,
|
||||
),
|
||||
|
||||
'email' => $this->options['email'] ?? text(
|
||||
label: 'Email address',
|
||||
required: true,
|
||||
validate: fn (string $email): ?string => match (true) {
|
||||
! filter_var($email, FILTER_VALIDATE_EMAIL) => 'The email address must be valid.',
|
||||
\Lunar\Admin\Models\Staff::where('email', $email)->exists() => 'A user with this email address already exists',
|
||||
default => null,
|
||||
},
|
||||
),
|
||||
|
||||
'admin' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ExportCleanupCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:export:cleanup {--keep=0 : Number of most recent exports to keep}";
|
||||
|
||||
protected $description = "Delete export archives";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$exportDir = Storage::disk("local")->path("exports");
|
||||
$files = glob($exportDir . "/export_*.zip");
|
||||
|
||||
if (empty($files)) {
|
||||
$this->info("No export files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
rsort($files);
|
||||
|
||||
$keep = max(0, (int) $this->option("keep"));
|
||||
$toDelete = array_slice($files, $keep);
|
||||
|
||||
if (empty($toDelete)) {
|
||||
$this->info("Nothing to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($toDelete as $file) {
|
||||
unlink($file);
|
||||
$this->line("Deleted: " . basename($file));
|
||||
}
|
||||
|
||||
$this->info("Deleted " . count($toDelete) . " export(s).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Core\ResultType\Error;
|
||||
use Modules\Core\ResultType\Result;
|
||||
use Modules\Core\ResultType\Success;
|
||||
use ZipArchive;
|
||||
|
||||
class ExportCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:export";
|
||||
|
||||
protected $description = "Export data and files";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$db = config("database.connections.pgsql");
|
||||
|
||||
$disk = Storage::disk("local");
|
||||
$exportDir = $disk->path("exports");
|
||||
if (!is_dir($exportDir)) {
|
||||
mkdir($exportDir, 0755, true);
|
||||
}
|
||||
|
||||
$stamp = now()->format("Y_m_d_His");
|
||||
$sqlFile = $exportDir . "/dump_{$stamp}.sql";
|
||||
$zipFile = $exportDir . "/export_{$stamp}.zip";
|
||||
$publicDisk = Storage::disk("public");
|
||||
$filesDir = $publicDisk->path("");
|
||||
|
||||
$result = $this->dumpDatabase($db, $sqlFile)->flatMap(
|
||||
fn($sql) => $this->buildZip($sql, $filesDir, $zipFile),
|
||||
);
|
||||
|
||||
if (file_exists($sqlFile)) {
|
||||
unlink($sqlFile);
|
||||
}
|
||||
|
||||
if ($result->error()->isDefined()) {
|
||||
$this->error($result->error()->get());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info("Exported to {$zipFile}");
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function dumpDatabase(array $db, string $sqlFile): Result
|
||||
{
|
||||
$command = sprintf(
|
||||
"PGPASSWORD=%s pg_dump -h %s -p %s -U %s -d %s --no-owner --no-acl > %s",
|
||||
$db["password"],
|
||||
$db["host"],
|
||||
$db["port"],
|
||||
$db["username"],
|
||||
$db["database"],
|
||||
$sqlFile,
|
||||
);
|
||||
|
||||
exec($command, result_code: $code);
|
||||
|
||||
if ($code !== 0) {
|
||||
return Error::create("pg_dump failed.");
|
||||
}
|
||||
|
||||
$this->info("Database dumped.");
|
||||
|
||||
return Success::create($sqlFile);
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function buildZip(
|
||||
string $sqlFile,
|
||||
string $filesDir,
|
||||
string $zipFile,
|
||||
): Result {
|
||||
$zip = new ZipArchive();
|
||||
if (
|
||||
$zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !==
|
||||
true
|
||||
) {
|
||||
return Error::create("Could not create zip archive.");
|
||||
}
|
||||
|
||||
$zip->addFile($sqlFile, basename($sqlFile));
|
||||
|
||||
if (is_dir($filesDir)) {
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator(
|
||||
$filesDir,
|
||||
\FilesystemIterator::SKIP_DOTS,
|
||||
),
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
$relativePath =
|
||||
"files/" .
|
||||
ltrim(
|
||||
str_replace($filesDir, "", $file->getRealPath()),
|
||||
DIRECTORY_SEPARATOR,
|
||||
);
|
||||
$zip->addFile($file->getRealPath(), $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return Success::create($zipFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Core\ResultType\Error;
|
||||
use Modules\Core\ResultType\Result;
|
||||
use Modules\Core\ResultType\Success;
|
||||
use ZipArchive;
|
||||
|
||||
class ImportCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:import";
|
||||
|
||||
protected $description = "Import data and files from an export archive";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!app()->environment("local")) {
|
||||
$this->error(
|
||||
"This command can only be run in the local environment.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$localDisk = Storage::disk("local");
|
||||
$exports = array_values(array_filter(
|
||||
$localDisk->files("exports"),
|
||||
fn($f) => fnmatch("exports/export_*.zip", $f),
|
||||
));
|
||||
|
||||
if (empty($exports)) {
|
||||
$this->error("No export files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
rsort($exports);
|
||||
$choices = array_map(fn($f) => basename($f), $exports);
|
||||
$chosen = $this->choice("Select an export file to import:", $choices, 0);
|
||||
$zipFile = $localDisk->path("exports/" . $chosen);
|
||||
|
||||
$db = config("database.connections.pgsql");
|
||||
$publicDisk = Storage::disk("public");
|
||||
$tempPath = "boboko_import_" . uniqid();
|
||||
|
||||
$result = $this->extractZip($zipFile, $localDisk, $tempPath)
|
||||
->flatMap(fn($path) => $this->restoreDatabase($db, $localDisk, $path))
|
||||
->flatMap(fn($path) => $this->restoreFiles($localDisk, $path, $publicDisk));
|
||||
|
||||
$localDisk->deleteDirectory($tempPath);
|
||||
|
||||
if ($result->error()->isDefined()) {
|
||||
$this->error($result->error()->get());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info("Import complete.");
|
||||
$this->call("boboko:anonymize");
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function extractZip(
|
||||
string $zipFile,
|
||||
Filesystem $localDisk,
|
||||
string $tempPath,
|
||||
): Result {
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFile) !== true) {
|
||||
return Error::create("Could not open zip archive.");
|
||||
}
|
||||
|
||||
$localDisk->makeDirectory($tempPath);
|
||||
$zip->extractTo($localDisk->path($tempPath));
|
||||
$zip->close();
|
||||
|
||||
$this->info("Archive extracted.");
|
||||
|
||||
return Success::create($tempPath);
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function restoreDatabase(
|
||||
array $db,
|
||||
Filesystem $localDisk,
|
||||
string $tempPath,
|
||||
): Result {
|
||||
$sqlFiles = array_values(array_filter(
|
||||
$localDisk->files($tempPath),
|
||||
fn($f) => fnmatch("dump_*.sql", basename($f)),
|
||||
));
|
||||
|
||||
if (empty($sqlFiles)) {
|
||||
return Error::create("No SQL dump found in archive.");
|
||||
}
|
||||
|
||||
$dropCommand = sprintf(
|
||||
"PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'",
|
||||
$db["password"],
|
||||
$db["host"],
|
||||
$db["port"],
|
||||
$db["username"],
|
||||
$db["database"],
|
||||
);
|
||||
|
||||
exec($dropCommand, result_code: $dropCode);
|
||||
|
||||
if ($dropCode !== 0) {
|
||||
return Error::create("Failed to reset database schema.");
|
||||
}
|
||||
|
||||
$command = sprintf(
|
||||
"PGPASSWORD=%s psql -h %s -p %s -U %s -d %s < %s",
|
||||
$db["password"],
|
||||
$db["host"],
|
||||
$db["port"],
|
||||
$db["username"],
|
||||
$db["database"],
|
||||
$localDisk->path($sqlFiles[0]),
|
||||
);
|
||||
|
||||
exec($command, result_code: $code);
|
||||
|
||||
if ($code !== 0) {
|
||||
return Error::create("psql restore failed.");
|
||||
}
|
||||
|
||||
$this->info("Database restored.");
|
||||
|
||||
return Success::create($tempPath);
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function restoreFiles(
|
||||
Filesystem $localDisk,
|
||||
string $tempPath,
|
||||
Filesystem $publicDisk,
|
||||
): Result {
|
||||
$filesSubPath = $tempPath . "/files";
|
||||
|
||||
if (!$localDisk->exists($filesSubPath)) {
|
||||
$this->info("No files to restore.");
|
||||
return Success::create($tempPath);
|
||||
}
|
||||
|
||||
foreach ($publicDisk->allFiles() as $file) {
|
||||
$publicDisk->delete($file);
|
||||
}
|
||||
foreach ($publicDisk->directories() as $dir) {
|
||||
$publicDisk->deleteDirectory($dir);
|
||||
}
|
||||
|
||||
$tempDisk = Storage::build([
|
||||
"driver" => "local",
|
||||
"root" => $localDisk->path($filesSubPath),
|
||||
]);
|
||||
|
||||
foreach ($tempDisk->allFiles() as $relativePath) {
|
||||
$publicDisk->put($relativePath, $tempDisk->readStream($relativePath));
|
||||
}
|
||||
|
||||
$this->info("Files restored.");
|
||||
|
||||
return Success::create($tempPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core;
|
||||
|
||||
use Filament\Contracts\Plugin;
|
||||
use Filament\Navigation\NavigationItem;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Lunar\Admin\Filament\Resources\StaffResource;
|
||||
use Lunar\Admin\Models\Staff as LunarStaff;
|
||||
use Lunar\Admin\Support\Facades\LunarPanel;
|
||||
use Modules\Core\Auth\Extensions\StaffResourceExtension;
|
||||
use Modules\Core\Auth\Filament\Pages\Login;
|
||||
use Modules\Core\Auth\Mail\InviteMail;
|
||||
|
||||
class CorePlugin implements Plugin
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'core';
|
||||
}
|
||||
|
||||
public function register(Panel $panel): void
|
||||
{
|
||||
$panel->path('boboko')
|
||||
->brandName('Boboko')
|
||||
->brandLogo(asset('logos/core/boboko-logo.svg'))
|
||||
->darkModeBrandLogo(asset('logos/core/boboko-logo-white.svg'))
|
||||
->login(Login::class)
|
||||
->navigationItems([
|
||||
NavigationItem::make("Lucent")
|
||||
->url("/lucent")
|
||||
->icon("heroicon-o-sun")
|
||||
->group("Content")
|
||||
->sort(1),
|
||||
]);
|
||||
|
||||
LunarPanel::extensions([
|
||||
StaffResource::class => StaffResourceExtension::class,
|
||||
]);
|
||||
|
||||
LunarStaff::addActivitylogExcept([
|
||||
'otp_code',
|
||||
'otp_expires_at',
|
||||
'password',
|
||||
'remember_token',
|
||||
'email_verified_at',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_confirmed_at',
|
||||
]);
|
||||
|
||||
LunarStaff::created(function (LunarStaff $staff) {
|
||||
Mail::to($staff->email)->send(new InviteMail($staff->first_name));
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(Panel $panel): void {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Customer\Extensions;
|
||||
|
||||
use Lunar\Admin\Filament\Resources\CustomerResource\RelationManagers\AddressRelationManager as BaseAddressRelationManager;
|
||||
use Lunar\Admin\Support\Extending\BaseExtension;
|
||||
use Modules\Core\Customer\RelationManagers\AddressRelationManager;
|
||||
|
||||
class CustomerResourceExtension extends BaseExtension
|
||||
{
|
||||
public function getRelations(array $relations): array
|
||||
{
|
||||
return array_map(
|
||||
fn ($relation) => $relation == BaseAddressRelationManager::class
|
||||
? AddressRelationManager::class
|
||||
: $relation,
|
||||
$relations
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Customer\Models;
|
||||
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Foundation\Auth\Access\Authorizable;
|
||||
|
||||
class Customer extends \Lunar\Models\Customer implements AuthenticatableContract
|
||||
{
|
||||
use Authenticatable;
|
||||
use Authorizable;
|
||||
|
||||
protected $casts = [
|
||||
'otp_expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function getTitleAttribute(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setTitleAttribute($value): void
|
||||
{
|
||||
// title is not used
|
||||
}
|
||||
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return trim(
|
||||
preg_replace(
|
||||
"/\s+/",
|
||||
" ",
|
||||
"{$this->first_name} {$this->last_name}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Customer\RelationManagers;
|
||||
|
||||
use Filament\Forms\Components\Group;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Lunar\Admin\Filament\Resources\CustomerResource\RelationManagers\AddressRelationManager as BaseAddressRelationManager;
|
||||
use Lunar\Models\Contracts\Address as AddressContract;
|
||||
|
||||
class AddressRelationManager extends BaseAddressRelationManager
|
||||
{
|
||||
public function getDefaultTable(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading(__('lunarpanel::address.plural_label'))
|
||||
->columns([
|
||||
TextColumn::make('line_one')->label(
|
||||
__('lunarpanel::address.table.line_one.label')
|
||||
),
|
||||
TextColumn::make('city')->label(
|
||||
__('lunarpanel::address.table.city.label')
|
||||
),
|
||||
TextColumn::make('postcode')->label(
|
||||
__('lunarpanel::address.table.postcode.label')
|
||||
),
|
||||
TextColumn::make('contact_email')->label(
|
||||
__('lunarpanel::address.table.contact_email.label')
|
||||
),
|
||||
TextColumn::make('contact_phone')->label(
|
||||
__('lunarpanel::address.table.contact_phone.label')
|
||||
),
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()->form($this->addressForm()),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make('editAddress')
|
||||
->fillForm(fn (AddressContract $record): array => [
|
||||
'line_one' => $record->line_one,
|
||||
'city' => $record->city,
|
||||
'state' => $record->state,
|
||||
'postcode' => $record->postcode,
|
||||
'country_id' => $record->country_id,
|
||||
'contact_email' => $record->contact_email,
|
||||
'contact_phone' => $record->contact_phone,
|
||||
])
|
||||
->form($this->addressForm()),
|
||||
DeleteAction::make('deleteAddress'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function addressForm(): array // phpcs:ignore
|
||||
{
|
||||
return [
|
||||
TextInput::make('line_one')->label(
|
||||
__('lunarpanel::address.form.line_one.label')
|
||||
)->required(),
|
||||
Group::make()->schema([
|
||||
Select::make('country_id')->label(
|
||||
__('lunarpanel::address.form.country_id.label')
|
||||
)->relationship(
|
||||
name: 'country',
|
||||
)->getOptionLabelFromRecordUsing(function (Model $record) {
|
||||
$name = $record->native ?: $record->name;
|
||||
|
||||
return "{$record->emoji} $name";
|
||||
}),
|
||||
TextInput::make('state')->label(
|
||||
__('lunarpanel::address.form.state.label')
|
||||
),
|
||||
])->columns(2),
|
||||
Group::make()->schema([
|
||||
TextInput::make('city')->label(
|
||||
__('lunarpanel::address.form.city.label')
|
||||
)->required(),
|
||||
TextInput::make('postcode')->label(
|
||||
__('lunarpanel::address.form.postcode.label')
|
||||
),
|
||||
])->columns(2),
|
||||
Group::make()->schema([
|
||||
TextInput::make('contact_email')->label(
|
||||
__('lunarpanel::address.form.contact_email.label')
|
||||
),
|
||||
TextInput::make('contact_phone')->label(
|
||||
__('lunarpanel::address.form.contact_phone.label')
|
||||
),
|
||||
])->columns(2),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Logging;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Thin wrapper around Spatie Activity Log that standardises the log channel,
|
||||
* actor (authenticated staff member), and property shape for all domain events.
|
||||
*
|
||||
* All logs are written to the 'lunar' channel. The subject is always an
|
||||
* Eloquent model, and the actor is resolved from the 'staff' guard at call time.
|
||||
*/
|
||||
class ActivityLogService
|
||||
{
|
||||
/**
|
||||
* Log a creation event. $attributes describes the initial state.
|
||||
*/
|
||||
public function created(Model $subject, array $attributes): void
|
||||
{
|
||||
activity('lunar')
|
||||
->performedOn($subject)
|
||||
->causedBy(auth('staff')->user())
|
||||
->withProperties(['attributes' => $attributes])
|
||||
->log('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an update event. $old holds the previous values, $attributes the new ones.
|
||||
*/
|
||||
public function updated(Model $subject, array $old, array $attributes): void
|
||||
{
|
||||
activity('lunar')
|
||||
->performedOn($subject)
|
||||
->causedBy(auth('staff')->user())
|
||||
->withProperties(['old' => $old, 'attributes' => $attributes])
|
||||
->log('updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a failed operation. $attributes provides context (e.g. error message, service).
|
||||
*/
|
||||
public function failed(Model $subject, array $attributes): void
|
||||
{
|
||||
activity('lunar')
|
||||
->performedOn($subject)
|
||||
->causedBy(auth('staff')->user())
|
||||
->withProperties(['attributes' => $attributes])
|
||||
->log('failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a deletion event. $attributes provides context (e.g. reason, name).
|
||||
*/
|
||||
public function deleted(Model $subject, array $attributes): void
|
||||
{
|
||||
activity('lunar')
|
||||
->performedOn($subject)
|
||||
->causedBy(auth('staff')->user())
|
||||
->withProperties(['attributes' => $attributes])
|
||||
->log('deleted');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Notification;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
abstract class BaseNotification extends Notification implements RegisterableNotification, ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Notification;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class NotificationRegistry
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
|
||||
private array $notifications = [];
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get(): static
|
||||
{
|
||||
if (static::$instance == null) {
|
||||
static::$instance = new static();
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
public function register(array $notifications): void
|
||||
{
|
||||
foreach ($notifications as $class) {
|
||||
$key = $class::getKey();
|
||||
$alreadyRegistered = isset($this->notifications[$key]);
|
||||
|
||||
$this->notifications[$key] = $class;
|
||||
|
||||
if ($alreadyRegistered) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Event::listen($class::listensTo(), function (object $event) use ($key) {
|
||||
$class = $this->notifications[$key] ?? null;
|
||||
if ($class == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$notification = new $class($event);
|
||||
if (isset($event->delaySeconds) && $event->delaySeconds > 0) {
|
||||
$notification->delay($event->delaySeconds);
|
||||
}
|
||||
$notification->notifiable()->notify($notification);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function unregister(string $key): void
|
||||
{
|
||||
unset($this->notifications[$key]);
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
return $this->notifications;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Notification;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\AnonymousNotifiable;
|
||||
|
||||
interface RegisterableNotification
|
||||
{
|
||||
public static function getKey(): string;
|
||||
|
||||
public static function listensTo(): string;
|
||||
|
||||
public function notifiable(): AnonymousNotifiable|Model;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Option;
|
||||
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Option<T>
|
||||
*/
|
||||
final class LazyOption extends Option
|
||||
{
|
||||
/** @var callable(mixed...):(Option<T>) */
|
||||
private $callback;
|
||||
|
||||
/** @var array<int, mixed> */
|
||||
private $arguments;
|
||||
|
||||
/** @var Option<T>|null */
|
||||
private $option;
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param callable(mixed...):(Option<S>) $callback
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @return LazyOption<S>
|
||||
*/
|
||||
public static function create($callback, array $arguments = []): self
|
||||
{
|
||||
return new self($callback, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(mixed...):(Option<T>) $callback
|
||||
* @param array<int, mixed> $arguments
|
||||
*/
|
||||
public function __construct($callback, array $arguments = [])
|
||||
{
|
||||
if (!is_callable($callback)) {
|
||||
throw new \InvalidArgumentException("Invalid callback given");
|
||||
}
|
||||
|
||||
$this->callback = $callback;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return $this->option()->isDefined();
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->option()->isEmpty();
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->option()->get();
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $this->option()->getOrElse($default);
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $this->option()->getOrCall($callable);
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
return $this->option()->getOrThrow($ex);
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $this->option()->orElse($else);
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
$this->option()->forAll($callable);
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
return $this->option()->forAll($callable);
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return $this->option()->map($callable);
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
return $this->option()->flatMap($callable);
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
return $this->option()->filter($callable);
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
return $this->option()->filterNot($callable);
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
return $this->option()->select($value);
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
return $this->option()->reject($value);
|
||||
}
|
||||
|
||||
/** @return Traversable<T> */
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->option()->getIterator();
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $this->option()->foldLeft($initialValue, $callable);
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $this->option()->foldRight($initialValue, $callable);
|
||||
}
|
||||
|
||||
/** @return Option<T> */
|
||||
private function option(): Option
|
||||
{
|
||||
if (null === $this->option) {
|
||||
/** @var mixed */
|
||||
$option = call_user_func_array($this->callback, $this->arguments);
|
||||
if ($option instanceof Option) {
|
||||
$this->option = $option;
|
||||
} else {
|
||||
throw new \RuntimeException(
|
||||
sprintf("Expected instance of %s", Option::class),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->option;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Option;
|
||||
|
||||
use EmptyIterator;
|
||||
|
||||
/**
|
||||
* @extends Option<mixed>
|
||||
*/
|
||||
final class None extends Option
|
||||
{
|
||||
/** @var None|null */
|
||||
private static $instance;
|
||||
|
||||
/** @return None */
|
||||
public static function create(): self
|
||||
{
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
throw new \RuntimeException("None has no value.");
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $callable();
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function orElse(Option $else)
|
||||
{
|
||||
return $else;
|
||||
}
|
||||
|
||||
public function ifDefined($callable)
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
public function forAll($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function map($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function flatMap($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIterator(): EmptyIterator
|
||||
{
|
||||
return new EmptyIterator();
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable)
|
||||
{
|
||||
return $initialValue;
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable)
|
||||
{
|
||||
return $initialValue;
|
||||
}
|
||||
|
||||
private function __construct() {}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Option;
|
||||
|
||||
use ArrayAccess;
|
||||
use IteratorAggregate;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @implements IteratorAggregate<T>
|
||||
*/
|
||||
abstract class Option implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* @template S
|
||||
*
|
||||
* @param S $value
|
||||
* @param S $noneValue
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
public static function fromValue($value, $noneValue = null)
|
||||
{
|
||||
if ($value === $noneValue) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template S
|
||||
*
|
||||
* @param array<string|int,S>|ArrayAccess<string|int,S>|null $array
|
||||
* @param string|int|null $key
|
||||
*
|
||||
* @return Option<S>
|
||||
*/
|
||||
public static function fromArraysValue($array, $key)
|
||||
{
|
||||
if (
|
||||
$key === null ||
|
||||
!(is_array($array) || $array instanceof ArrayAccess) ||
|
||||
!isset($array[$key])
|
||||
) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($array[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template S
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param array $arguments
|
||||
* @param S $noneValue
|
||||
*
|
||||
* @return LazyOption<S>
|
||||
*/
|
||||
public static function fromReturn(
|
||||
$callback,
|
||||
array $arguments = [],
|
||||
$noneValue = null,
|
||||
) {
|
||||
return new LazyOption(static function () use (
|
||||
$callback,
|
||||
$arguments,
|
||||
$noneValue,
|
||||
) {
|
||||
/** @var mixed */
|
||||
$return = call_user_func_array($callback, $arguments);
|
||||
|
||||
if ($return === $noneValue) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return new Some($return);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template S
|
||||
*
|
||||
* @param Option<S>|callable|S $value
|
||||
* @param S $noneValue
|
||||
*
|
||||
* @return Option<S>|LazyOption<S>
|
||||
*/
|
||||
public static function ensure($value, $noneValue = null)
|
||||
{
|
||||
if ($value instanceof self) {
|
||||
return $value;
|
||||
} elseif (is_callable($value)) {
|
||||
return new LazyOption(static function () use ($value, $noneValue) {
|
||||
/** @var mixed */
|
||||
$return = $value();
|
||||
|
||||
if ($return instanceof self) {
|
||||
return $return;
|
||||
} else {
|
||||
return self::fromValue($return, $noneValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self::fromValue($value, $noneValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template S
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param mixed $noneValue
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public static function lift($callback, $noneValue = null)
|
||||
{
|
||||
return static function () use ($callback, $noneValue) {
|
||||
/** @var array<int, mixed> */
|
||||
$args = func_get_args();
|
||||
|
||||
$reduced_args = array_reduce(
|
||||
$args,
|
||||
/** @param bool $status */
|
||||
static function ($status, self $o) {
|
||||
return $o->isEmpty() ? true : $status;
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
if ($reduced_args) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
$args = array_map(static function (self $o) {
|
||||
return $o->get();
|
||||
}, $args);
|
||||
|
||||
return self::ensure(
|
||||
call_user_func_array($callback, $args),
|
||||
$noneValue,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/** @return T */
|
||||
abstract public function get();
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param S $default
|
||||
* @return T|S
|
||||
*/
|
||||
abstract public function getOrElse($default);
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param callable():S $callable
|
||||
* @return T|S
|
||||
*/
|
||||
abstract public function getOrCall($callable);
|
||||
|
||||
/** @return T */
|
||||
abstract public function getOrThrow(\Exception $ex);
|
||||
|
||||
abstract public function isEmpty(): bool;
|
||||
|
||||
abstract public function isDefined(): bool;
|
||||
|
||||
/**
|
||||
* @param Option<T> $else
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function orElse(self $else);
|
||||
|
||||
/** @deprecated Use forAll() instead. */
|
||||
abstract public function ifDefined($callable);
|
||||
|
||||
/**
|
||||
* @param callable(T):mixed $callable
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function forAll($callable);
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param callable(T):S $callable
|
||||
* @return Option<S>
|
||||
*/
|
||||
abstract public function map($callable);
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param callable(T):Option<S> $callable
|
||||
* @return Option<S>
|
||||
*/
|
||||
abstract public function flatMap($callable);
|
||||
|
||||
/**
|
||||
* @param callable(T):bool $callable
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function filter($callable);
|
||||
|
||||
/**
|
||||
* @param callable(T):bool $callable
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function filterNot($callable);
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function select($value);
|
||||
|
||||
/**
|
||||
* @param T $value
|
||||
* @return Option<T>
|
||||
*/
|
||||
abstract public function reject($value);
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param S $initialValue
|
||||
* @param callable(S, T):S $callable
|
||||
* @return S
|
||||
*/
|
||||
abstract public function foldLeft($initialValue, $callable);
|
||||
|
||||
/**
|
||||
* @template S
|
||||
* @param S $initialValue
|
||||
* @param callable(T, S):S $callable
|
||||
* @return S
|
||||
*/
|
||||
abstract public function foldRight($initialValue, $callable);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Option;
|
||||
|
||||
use ArrayIterator;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*
|
||||
* @extends Option<T>
|
||||
*/
|
||||
final class Some extends Option
|
||||
{
|
||||
/** @var T */
|
||||
private $value;
|
||||
|
||||
/** @param T $value */
|
||||
public function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template U
|
||||
* @param U $value
|
||||
* @return Some<U>
|
||||
*/
|
||||
public static function create($value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public function isDefined(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrElse($default)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrCall($callable)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getOrThrow(\Exception $ex)
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function orElse(Option $else): Some
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ifDefined($callable): void
|
||||
{
|
||||
$this->forAll($callable);
|
||||
}
|
||||
|
||||
public function forAll($callable): Some
|
||||
{
|
||||
$callable($this->value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function map($callable): Some
|
||||
{
|
||||
return new self($callable($this->value));
|
||||
}
|
||||
|
||||
public function flatMap($callable): Option
|
||||
{
|
||||
/** @var mixed */
|
||||
$rs = $callable($this->value);
|
||||
if (!$rs instanceof Option) {
|
||||
throw new \RuntimeException(
|
||||
"Callables passed to flatMap() must return an Option. Maybe you should use map() instead?",
|
||||
);
|
||||
}
|
||||
|
||||
return $rs;
|
||||
}
|
||||
|
||||
public function filter($callable)
|
||||
{
|
||||
if (true === $callable($this->value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function filterNot($callable)
|
||||
{
|
||||
if (false === $callable($this->value)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function select($value)
|
||||
{
|
||||
if ($this->value === $value) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return None::create();
|
||||
}
|
||||
|
||||
public function reject($value)
|
||||
{
|
||||
if ($this->value === $value) {
|
||||
return None::create();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return ArrayIterator<int, T> */
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
return new ArrayIterator([$this->value]);
|
||||
}
|
||||
|
||||
public function foldLeft($initialValue, $callable): S
|
||||
{
|
||||
return $callable($initialValue, $this->value);
|
||||
}
|
||||
|
||||
public function foldRight($initialValue, $callable): S
|
||||
{
|
||||
return $callable($this->value, $initialValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Lunar\Admin\Filament\Resources\StaffResource;
|
||||
use Lunar\Admin\Models\Staff as LunarStaff;
|
||||
use Lunar\Admin\Support\Facades\LunarPanel;
|
||||
use Lunar\Models\Customer as LunarCustomer;
|
||||
use Modules\Core\Auth\Extensions\StaffResourceExtension;
|
||||
use Modules\Core\Auth\Mail\InviteMail;
|
||||
use Modules\Core\Command\CreateAdminCommand;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
LunarPanel::extensions([
|
||||
StaffResource::class => StaffResourceExtension::class,
|
||||
]);
|
||||
|
||||
LunarStaff::addActivitylogExcept([
|
||||
'otp_code',
|
||||
'otp_expires_at',
|
||||
'password',
|
||||
'remember_token',
|
||||
'email_verified_at',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_confirmed_at',
|
||||
]);
|
||||
|
||||
LunarCustomer::addActivitylogExcept([
|
||||
'otp_code',
|
||||
'otp_expires_at',
|
||||
]);
|
||||
|
||||
LunarStaff::created(function (LunarStaff $staff) {
|
||||
Mail::to($staff->email)->send(new InviteMail($staff->first_name));
|
||||
});
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->app->booted(fn () => $this->commands([CreateAdminCommand::class]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Core\Command\AnonymizeCommand;
|
||||
use Modules\Core\Command\ExportCleanupCommand;
|
||||
use Modules\Core\Command\ExportCommand;
|
||||
use Modules\Core\Command\ImportCommand;
|
||||
|
||||
class CoreServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void {}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__ . '/../../resources/views', 'core');
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__ . '/../../resources/logos' => public_path('logos/core'),
|
||||
], 'core-assets');
|
||||
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([AnonymizeCommand::class, ExportCommand::class, ExportCleanupCommand::class, ImportCommand::class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Lunar\Admin\Filament\Resources\CustomerResource;
|
||||
use Lunar\Admin\Support\Facades\LunarPanel;
|
||||
use Lunar\Facades\ModelManifest;
|
||||
use Lunar\Models\Contracts\Customer as LunarCustomer;
|
||||
use Modules\Core\Customer\Extensions\CustomerResourceExtension;
|
||||
use Modules\Core\Customer\Models\Customer;
|
||||
|
||||
class CustomerServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
LunarPanel::extensions([
|
||||
CustomerResource::class => CustomerResourceExtension::class,
|
||||
]);
|
||||
|
||||
ModelManifest::replace(LunarCustomer::class, Customer::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Core\ResultType;
|
||||
|
||||
use Modules\Core\Option\None;
|
||||
use Modules\Core\Option\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*
|
||||
* @extends \Modules\Core\ResultType\Result<T,E>
|
||||
*/
|
||||
final class Error extends Result
|
||||
{
|
||||
/**
|
||||
* @var E
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* Internal constructor for an error value.
|
||||
*
|
||||
* @param E $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param F $value
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<T,F>
|
||||
*/
|
||||
public static function create($value): Error
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \Modules\Core\Option\Option<T>
|
||||
*/
|
||||
public function success()
|
||||
{
|
||||
return None::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,E>
|
||||
*/
|
||||
public function map(callable $f): Result
|
||||
{
|
||||
return self::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\Modules\Core\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,F>
|
||||
*/
|
||||
public function flatMap(callable $f): Result
|
||||
{
|
||||
/** @var \Modules\Core\ResultType\Result<S,F> */
|
||||
return self::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \Modules\Core\Option\Option<E>
|
||||
*/
|
||||
public function error(): Some
|
||||
{
|
||||
return Some::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<T,F>
|
||||
*/
|
||||
public function mapError(callable $f): Result
|
||||
{
|
||||
return self::create($f($this->value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Core\ResultType;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*/
|
||||
abstract class Result
|
||||
{
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \Modules\Core\Option\Option<T>
|
||||
*/
|
||||
abstract public function success();
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,E>
|
||||
*/
|
||||
abstract public function map(callable $f);
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\Modules\Core\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,F>
|
||||
*/
|
||||
abstract public function flatMap(callable $f);
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \Modules\Core\Option\Option<E>
|
||||
*/
|
||||
abstract public function error();
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<T,F>
|
||||
*/
|
||||
abstract public function mapError(callable $f);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Core\ResultType;
|
||||
|
||||
use Modules\Core\Option\None;
|
||||
use Modules\Core\Option\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*
|
||||
* @extends \Modules\Core\ResultType\Result<T,E>
|
||||
*/
|
||||
final class Success extends Result
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* Internal constructor for a success value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $value
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,E>
|
||||
*/
|
||||
public static function create($value): Success
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \Modules\Core\Option\Option<T>
|
||||
*/
|
||||
public function success(): Some
|
||||
{
|
||||
return Some::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,E>
|
||||
*/
|
||||
public function map(callable $f): Result
|
||||
{
|
||||
return self::create($f($this->value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\Modules\Core\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<S,F>
|
||||
*/
|
||||
public function flatMap(callable $f)
|
||||
{
|
||||
return $f($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \Modules\Core\Option\Option<E>
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return None::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \Modules\Core\ResultType\Result<T,F>
|
||||
*/
|
||||
public function mapError(callable $f): Result
|
||||
{
|
||||
return self::create($this->value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user