feat: add app and database modules

This commit is contained in:
2026-05-21 16:05:11 +07:00
parent 37b7e783f5
commit fad70d096b
212 changed files with 23901 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace App\Services\Auth;
use App\Models\OtpCode;
use Carbon\Carbon;
class OtpService
{
/**
* Generate and save an OTP code for an identifier.
*/
public function generate(string $identifier, int $expiryMinutes = 10): string
{
// Delete old unexpired codes for this identifier to avoid clutter
OtpCode::where('identifier', $identifier)
->whereNull('verified_at')
->delete();
$code = (string) random_int(100000, 999999);
OtpCode::create([
'identifier' => $identifier,
'code' => $code,
'expires_at' => Carbon::now()->addMinutes($expiryMinutes),
]);
return $code;
}
/**
* Verify the OTP code.
*/
public function verify(string $identifier, string $code): bool
{
$otp = OtpCode::where('identifier', $identifier)
->where('code', $code)
->whereNull('verified_at')
->where('expires_at', '>', Carbon::now())
->latest()
->first();
if ($otp) {
$otp->update(['verified_at' => Carbon::now()]);
return true;
}
return false;
}
/**
* Clear expired codes.
*/
public function cleanup(): void
{
OtpCode::where('expires_at', '<', Carbon::now())
->whereNull('verified_at')
->delete();
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace App\Services\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
class PasswordPolicyService
{
/**
* Get the password validation rules based on system settings.
*/
public static function getRules(): Password
{
$rules = Password::min(get_setting('password_min_length', 8))
->max(get_setting('password_max_length', 64));
$requireUpper = get_setting('password_require_uppercase', false);
$requireLower = get_setting('password_require_lowercase', false);
if ($requireUpper && $requireLower) {
$rules->mixedCase();
} elseif ($requireUpper) {
$rules->rules(['regex:/[A-Z]/']);
} elseif ($requireLower) {
$rules->rules(['regex:/[a-z]/']);
}
if (get_setting('password_require_numeric', false)) {
$rules->numbers();
}
if (get_setting('password_require_special', false)) {
$rules->symbols();
}
return $rules;
}
/**
* Check if the password has expired for a user.
*/
public static function isPasswordExpired(User $user): bool
{
$expiryDays = get_setting('password_expiry_days', 0);
if ($expiryDays <= 0) {
return false;
}
$lastChanged = $user->password_changed_at ?? $user->created_at;
return $lastChanged->addDays($expiryDays)->isPast();
}
/**
* Verify that the new password is not in the user's password history.
*/
public static function checkHistory(User $user, string $newPassword): void
{
$historyCount = get_setting('password_history_count', 0);
if ($historyCount <= 0) {
return;
}
$histories = $user->passwordHistories()
->latest()
->take($historyCount)
->get();
foreach ($histories as $history) {
if (Hash::check($newPassword, $history->password)) {
throw ValidationException::withMessages([
'password' => __('You cannot reuse any of your last :count passwords.', ['count' => $historyCount]),
]);
}
}
}
/**
* Record the current password into history and update changed_at timestamp.
*/
public static function recordPasswordChange(User $user, string $newPasswordHash): void
{
$historyCount = get_setting('password_history_count', 0);
// 1. Record to history (only if enabled)
if ($historyCount > 0) {
$user->passwordHistories()->create([
'password' => $newPasswordHash,
]);
}
// 2. Update timestamp
$user->update([
'password_changed_at' => now(),
]);
// 3. Optional: Prune old history — keep exactly $historyCount entries
$historyCount = get_setting('password_history_count', 0);
if ($historyCount > 0) {
$user->passwordHistories()
->orderBy('created_at', 'desc')
->skip($historyCount)
->take(100)
->delete();
}
}
}