- Dodano PSR-4 autoloader do wszystkich 6 punktów wejścia - Shared\: CacheHandler, Helpers, Html, ImageManipulator, Tpl - Domain\: LanguagesRepository, SettingsRepository, UserRepository - Stare class.*.php → cienkie wrappery (kompatybilność wsteczna) - Dodano dokumentację: docs/PROJECT_STRUCTURE.md + pozostałe docs/ - Dodano CLAUDE.md z workflow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
236 lines
7.9 KiB
PHP
236 lines
7.9 KiB
PHP
<?php
|
||
namespace Domain\User;
|
||
|
||
class UserRepository
|
||
{
|
||
private $db;
|
||
|
||
public function __construct( $db )
|
||
{
|
||
$this->db = $db;
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Odczyt
|
||
// -------------------------------------------------------------------------
|
||
|
||
public function find( int $userId ): ?array
|
||
{
|
||
return $this->db->get( 'pp_users', '*', [ 'id' => $userId ] ) ?: null;
|
||
}
|
||
|
||
public function findByLogin( string $login ): ?array
|
||
{
|
||
return $this->db->get( 'pp_users', '*', [ 'login' => $login ] ) ?: null;
|
||
}
|
||
|
||
public function all(): array
|
||
{
|
||
return $this->db->select( 'pp_users', '*' ) ?: [];
|
||
}
|
||
|
||
public function privileges( int $userId ): array
|
||
{
|
||
return $this->db->select( 'pp_users_privileges', '*', [ 'id_user' => $userId ] ) ?: [];
|
||
}
|
||
|
||
public function hasPrivilege( string $name, int $userId ): bool
|
||
{
|
||
if ( $userId === 1 )
|
||
return true;
|
||
|
||
if ( !$result = \Shared\Cache\CacheHandler::fetch( "check_privileges:$userId:$name-tmp" ) )
|
||
{
|
||
$result = $this->db->count( 'pp_users_privileges', [ 'AND' => [ 'name' => $name, 'id_user' => $userId ] ] );
|
||
\Shared\Cache\CacheHandler::store( "check_privileges:$userId:$name", $result );
|
||
}
|
||
return (bool) $result;
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Logowanie
|
||
// -------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Weryfikuje login i hasło.
|
||
* @return int 1 = OK, 0 = złe dane, -1 = konto zablokowane
|
||
*/
|
||
public function logon( string $login, string $password ): int
|
||
{
|
||
if ( !$this->db->get( 'pp_users', '*', [ 'login' => $login ] ) )
|
||
return 0;
|
||
|
||
if ( !$this->db->get( 'pp_users', '*', [ 'AND' => [ 'login' => $login, 'status' => 1, 'error_logged_count[<]' => 5 ] ] ) )
|
||
return -1;
|
||
|
||
if ( $this->db->get( 'pp_users', '*', [
|
||
'AND' => [
|
||
'login' => $login,
|
||
'status' => 1,
|
||
'password' => md5( $password ),
|
||
'OR' => [ 'active_to[>=]' => date( 'Y-m-d' ), 'active_to' => null ]
|
||
]
|
||
] ) ) {
|
||
$this->db->update( 'pp_users', [ 'last_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count' => 0 ], [ 'login' => $login ] );
|
||
return 1;
|
||
}
|
||
|
||
$this->db->update( 'pp_users', [ 'last_error_logged' => date( 'Y-m-d H:i:s' ), 'error_logged_count[+]' => 1 ], [ 'login' => $login ] );
|
||
if ( $this->db->get( 'pp_users', 'error_logged_count', [ 'login' => $login ] ) >= 5 )
|
||
{
|
||
$this->db->update( 'pp_users', [ 'status' => 0 ], [ 'login' => $login ] );
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
public function isLoginTaken( string $login, int $excludeId = 0 ): bool
|
||
{
|
||
return (bool) $this->db->get( 'pp_users', 'login', [ 'AND' => [ 'login' => $login, 'id[!]' => $excludeId ] ] );
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 2FA
|
||
// -------------------------------------------------------------------------
|
||
|
||
public function update( int $userId, array $data ): bool
|
||
{
|
||
return (bool) $this->db->update( 'pp_users', $data, [ 'id' => $userId ] );
|
||
}
|
||
|
||
public function sendTwofaCode( int $userId, bool $resend = false ): bool
|
||
{
|
||
$user = $this->find( $userId );
|
||
if ( !$user ) return false;
|
||
|
||
if ( (int)$user['twofa_enabled'] !== 1 ) return false;
|
||
|
||
$to = $user['twofa_email'] ?: $user['login'];
|
||
if ( !filter_var( $to, FILTER_VALIDATE_EMAIL ) ) return false;
|
||
|
||
if ( $resend && !empty( $user['twofa_sent_at'] ) )
|
||
{
|
||
$last = strtotime( $user['twofa_sent_at'] );
|
||
if ( $last && ( time() - $last ) < 30 ) return false;
|
||
}
|
||
|
||
$code = random_int( 100000, 999999 );
|
||
$hash = password_hash( (string)$code, PASSWORD_DEFAULT );
|
||
|
||
$this->update( $userId, [
|
||
'twofa_code_hash' => $hash,
|
||
'twofa_expires_at' => date( 'Y-m-d H:i:s', time() + 10 * 60 ),
|
||
'twofa_sent_at' => date( 'Y-m-d H:i:s' ),
|
||
'twofa_failed_attempts' => 0,
|
||
] );
|
||
|
||
$subject = 'Twój kod logowania 2FA';
|
||
$body = "Twój kod logowania do panelu administratora: {$code}. Kod jest ważny przez 10 minut. Jeśli to nie Ty inicjowałeś logowanie – zignoruj tę wiadomość i poinformuj administratora.";
|
||
|
||
$sent = \S::send_email( $to, $subject, $body );
|
||
if ( !$sent )
|
||
{
|
||
$headers = "MIME-Version: 1.0\r\n";
|
||
$headers .= "Content-type: text/plain; charset=UTF-8\r\n";
|
||
$headers .= "From: no-reply@" . ( $_SERVER['HTTP_HOST'] ?? 'localhost' ) . "\r\n";
|
||
$sent = mail( $to, mb_encode_mimeheader( $subject, 'UTF-8' ), $body, $headers );
|
||
}
|
||
return (bool) $sent;
|
||
}
|
||
|
||
public function verifyTwofaCode( int $userId, string $code ): bool
|
||
{
|
||
$user = $this->find( $userId );
|
||
if ( !$user ) return false;
|
||
|
||
if ( (int)$user['twofa_failed_attempts'] >= 5 ) return false;
|
||
|
||
if ( empty( $user['twofa_expires_at'] ) || time() > strtotime( $user['twofa_expires_at'] ) )
|
||
{
|
||
$this->update( $userId, [ 'twofa_code_hash' => null, 'twofa_expires_at' => null ] );
|
||
return false;
|
||
}
|
||
|
||
$ok = !empty( $user['twofa_code_hash'] ) && password_verify( $code, $user['twofa_code_hash'] );
|
||
if ( $ok )
|
||
{
|
||
$this->update( $userId, [
|
||
'twofa_code_hash' => null,
|
||
'twofa_expires_at' => null,
|
||
'twofa_sent_at' => null,
|
||
'twofa_failed_attempts' => 0,
|
||
'last_logged' => date( 'Y-m-d H:i:s' ),
|
||
] );
|
||
return true;
|
||
}
|
||
|
||
$this->update( $userId, [
|
||
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
|
||
'last_error_logged' => date( 'Y-m-d H:i:s' ),
|
||
] );
|
||
return false;
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Zapis / usuwanie
|
||
// -------------------------------------------------------------------------
|
||
|
||
public function save(
|
||
$userId, string $login, $status, $activeTo, string $password, string $passwordRe,
|
||
$admin, $privileges, $twofaEnabled = 0, string $twofaEmail = ''
|
||
): array {
|
||
$this->db->delete( 'pp_users_privileges', [ 'id_user' => (int)$userId ] );
|
||
|
||
if ( !$userId )
|
||
{
|
||
if ( strlen( $password ) < 5 )
|
||
return [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
|
||
if ( $password !== $passwordRe )
|
||
return [ 'status' => 'error', 'msg' => 'Podane hasła są różne.' ];
|
||
|
||
$this->db->insert( 'pp_users', [
|
||
'login' => $login,
|
||
'status' => $status == 'on' ? 1 : 0,
|
||
'active_to' => $activeTo === '' ? null : $activeTo,
|
||
'admin' => $admin,
|
||
'password' => md5( $password ),
|
||
'twofa_enabled' => $twofaEnabled == 'on' ? 1 : 0,
|
||
'twofa_email' => $twofaEmail,
|
||
] );
|
||
$userId = $this->db->get( 'pp_users', 'id', [ 'ORDER' => [ 'id' => 'DESC' ] ] );
|
||
}
|
||
else
|
||
{
|
||
if ( $password && strlen( $password ) < 5 )
|
||
return [ 'status' => 'error', 'msg' => 'Podane hasło jest zbyt krótkie.' ];
|
||
if ( $password && $password !== $passwordRe )
|
||
return [ 'status' => 'error', 'msg' => 'Podane hasła są różne.' ];
|
||
|
||
if ( $password )
|
||
$this->db->update( 'pp_users', [ 'password' => md5( $password ) ], [ 'id' => (int)$userId ] );
|
||
|
||
$this->db->update( 'pp_users', [
|
||
'login' => $login,
|
||
'admin' => $admin,
|
||
'status' => $status == 'on' ? 1 : 0,
|
||
'active_to' => $activeTo === '' ? null : $activeTo,
|
||
'error_logged_count' => 0,
|
||
'twofa_enabled' => $twofaEnabled == 'on' ? 1 : 0,
|
||
'twofa_email' => $twofaEmail,
|
||
], [ 'id' => (int)$userId ] );
|
||
}
|
||
|
||
$privileges = (array)$privileges;
|
||
foreach ( $privileges as $pri )
|
||
$this->db->insert( 'pp_users_privileges', [ 'name' => $pri, 'id_user' => $userId ] );
|
||
|
||
\S::delete_cache();
|
||
return [ 'status' => 'ok', 'msg' => 'Użytkownik został zapisany.' ];
|
||
}
|
||
|
||
public function delete( int $userId ): bool
|
||
{
|
||
return (bool) $this->db->delete( 'pp_users', [ 'id' => $userId ] );
|
||
}
|
||
}
|