First public release. - docs: document the audit log, user offboarding, CSV export and the "require a reason for destructive actions" setting across README, docs/ and CLAUDE/AGENTS.md; note retention for panel_audit - docs: fill the publishing placeholders (repo URL, security contact, issue-template links) and add a semantic-versioning policy - move agent/contributor context into CLAUDE/ (AGENTS.md + a README); root CLAUDE.md is now a thin @-import pointer - add a favicon (shield-lock, matching the login screen) to the login page and the app layout - CHANGELOG: promote [Unreleased] to [1.0.0]; keep the earlier internal build as [0.1.0] Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfuvfwexztdS7LDpGuKsuK
115 lines
3.3 KiB
PHP
115 lines
3.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Illuminate\View\View;
|
|
use LdapRecord\LdapRecordException;
|
|
|
|
/**
|
|
* Logowanie do panelu kontem Active Directory.
|
|
*
|
|
* Guard 'ldap' (LdapRecord "plain bind") sprawdza login + hasło bindem do DC,
|
|
* a dostęp do panelu wymusza reguła MemberOfLoginGroup (członkostwo w grupie
|
|
* z AD_LOGIN_GROUP). Nie prowadzimy lokalnej tabeli users - po zalogowaniu
|
|
* profil admina ląduje w sesji pod kluczem 'ad_admin'.
|
|
*/
|
|
class LoginController extends Controller
|
|
{
|
|
/**
|
|
* Maksymalna liczba nieudanych prób logowania (login + IP) zanim
|
|
* zadziała throttling.
|
|
*/
|
|
private const MAX_ATTEMPTS = 5;
|
|
|
|
public function show(): View
|
|
{
|
|
return view('auth.login');
|
|
}
|
|
|
|
public function login(Request $request): RedirectResponse
|
|
{
|
|
$credentials = $request->validate([
|
|
'login' => ['required', 'string', 'max:255'],
|
|
'password' => ['required', 'string'],
|
|
], attributes: [
|
|
'login' => __('auth.attributes.login'),
|
|
'password' => __('auth.attributes.password'),
|
|
]);
|
|
|
|
$this->ensureIsNotRateLimited($request);
|
|
|
|
try {
|
|
$authenticated = Auth::guard('ldap')->attempt([
|
|
'samaccountname' => $credentials['login'],
|
|
'password' => $credentials['password'],
|
|
]);
|
|
} catch (LdapRecordException $e) {
|
|
report($e);
|
|
|
|
throw ValidationException::withMessages([
|
|
'login' => __('auth.failed_dc'),
|
|
]);
|
|
}
|
|
|
|
if (! $authenticated) {
|
|
RateLimiter::hit($this->throttleKey($request));
|
|
|
|
throw ValidationException::withMessages([
|
|
'login' => __('auth.failed_credentials'),
|
|
]);
|
|
}
|
|
|
|
RateLimiter::clear($this->throttleKey($request));
|
|
|
|
$request->session()->regenerate();
|
|
|
|
$request->session()->put(
|
|
'ad_admin',
|
|
Auth::guard('ldap')->user()->toPanelProfile()
|
|
);
|
|
|
|
return redirect()->route('dashboard.index');
|
|
}
|
|
|
|
public function logout(Request $request): RedirectResponse
|
|
{
|
|
Auth::guard('ldap')->logout();
|
|
|
|
$request->session()->forget('ad_admin');
|
|
$request->session()->invalidate();
|
|
$request->session()->regenerateToken();
|
|
|
|
return redirect()->route('login')->with('status', __('auth.logged_out'));
|
|
}
|
|
|
|
/**
|
|
* @throws ValidationException
|
|
*/
|
|
protected function ensureIsNotRateLimited(Request $request): void
|
|
{
|
|
if (! RateLimiter::tooManyAttempts($this->throttleKey($request), self::MAX_ATTEMPTS)) {
|
|
return;
|
|
}
|
|
|
|
$seconds = RateLimiter::availableIn($this->throttleKey($request));
|
|
|
|
throw ValidationException::withMessages([
|
|
'login' => __('auth.throttled', ['seconds' => $seconds]),
|
|
]);
|
|
}
|
|
|
|
protected function throttleKey(Request $request): string
|
|
{
|
|
return Str::transliterate(
|
|
Str::lower((string) $request->input('login')).'|'.$request->ip()
|
|
);
|
|
}
|
|
}
|