This commit is contained in:
Konstantinos Arvanitakis
2026-07-01 18:35:39 +03:00
commit 9f58a36c82
44 changed files with 3848 additions and 0 deletions
@@ -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);
}
}
+97
View File
@@ -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';
}
}
+25
View File
@@ -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');
}
}
+24
View File
@@ -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');
}
}
+25
View File
@@ -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');
}
}
+24
View File
@@ -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',
];
}
+61
View File
@@ -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();
}
}
+51
View File
@@ -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;
}
}