Add two-factor authentication fields to pp_users table and update .htaccess for security
- Added columns for two-factor authentication (2FA) in the pp_users table: - twofa_enabled (TINYINT) - twofa_email (VARCHAR) - twofa_code_hash (VARCHAR) - twofa_expires_at (DATETIME) - twofa_sent_at (DATETIME) - twofa_failed_attempts (INT) - Updated the twofa_enabled and twofa_email for user with id 0. - Enhanced .htaccess to disable directory listing, block execution of sensitive files, and prevent serving hidden files.
This commit is contained in:
@@ -85,5 +85,22 @@ $user = \S::get_session( 'user', true );
|
|||||||
\admin\Site::update();
|
\admin\Site::update();
|
||||||
\admin\Site::special_actions();
|
\admin\Site::special_actions();
|
||||||
|
|
||||||
|
$domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
|
||||||
|
$cookie_name = str_replace( '.', '-', $domain );
|
||||||
|
|
||||||
|
if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) )
|
||||||
|
{
|
||||||
|
$obj = json_decode( $_COOKIE[$cookie_name] );
|
||||||
|
$login = $obj -> {'login'};
|
||||||
|
$password = $obj -> {'hash'};
|
||||||
|
|
||||||
|
if ( $mdb -> get( 'pp_users', '*', [ 'AND' => [ 'login' => $login, 'status' => 1, 'password' => $password ] ] ) )
|
||||||
|
{
|
||||||
|
\S::set_session( 'user', \admin\factory\Users::details( $login ) );
|
||||||
|
header( 'Location: /admin/articles/view_list/' );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
echo \admin\view\Page::show();
|
echo \admin\view\Page::show();
|
||||||
?>
|
?>
|
||||||
20
admin/layout/.htaccess
Normal file
20
admin/layout/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
20
admin/templates/.htaccess
Normal file
20
admin/templates/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
@@ -63,6 +63,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col col-sm-12" style="align-items: center; display: flex;">
|
||||||
|
<input id="remember" type="checkbox" name="remember" value="1">
|
||||||
|
<label class="remember" style="margin-bottom: 0; margin-left: 5px;"> Pamiętaj mnie 14 dni</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="sumbit">
|
<div class="sumbit">
|
||||||
<button class="btn">Zaloguj się</button>
|
<button class="btn">Zaloguj się</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
60
admin/templates/site/unlogged.php
Normal file
60
admin/templates/site/unlogged.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>shopPro</title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="author" content="www.project-pro.pl - internetowe rozwiązania dla biznesu">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/libraries/bootstrap-4.5.2-dist/css/bootstrap.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/libraries/font-awesome-4.7.0/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="/admin/layout/style-css/style.css">
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;0,700;0,800;1,300;1,400;1,600;1,700;1,800&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-8 col-md-6 col-lg-4 unlogged-panel">
|
||||||
|
<div class="login-panel">
|
||||||
|
<div class="login-panel-content">
|
||||||
|
<div class="login-shoppro">
|
||||||
|
shop<b>Pro</b>
|
||||||
|
</div>
|
||||||
|
<div class="title">
|
||||||
|
Witaj ponownie!
|
||||||
|
</div>
|
||||||
|
<div class="subtitle">
|
||||||
|
Zaloguj się do panelu administratora <b>shopPro</b>.
|
||||||
|
</div>
|
||||||
|
<?
|
||||||
|
if ( $alert = \S::get_session( 'alert' ) ):
|
||||||
|
\S::alert( false );
|
||||||
|
?>
|
||||||
|
<div class="alert alert-danger alert-dismissable">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
|
<i class="icon fa fa-ban "></i><?= $alert;?>
|
||||||
|
</div>
|
||||||
|
<? endif;
|
||||||
|
?>
|
||||||
|
<?= $this -> content; ?>
|
||||||
|
<div class="copyright">
|
||||||
|
<a href="https://www.project-pro.pl/"> © <?= date( 'Y' );?> Project-Pro</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4 col-md-6 col-lg-8 unlogged-bg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="home">
|
||||||
|
<a href="/"><img src="/admin/layout/images/home.svg" alt="HOME"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
admin/templates/users/user-2fa.php
Normal file
18
admin/templates/users/user-2fa.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<form method="POST" action="/admin/" class="form-horizontal" rol="form">
|
||||||
|
<input type="hidden" name="s-action" value="user-2fa-verify">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col col-sm-4 control-label" for="login">Kod z e-maila:</label>
|
||||||
|
<div class="col col-sm-8">
|
||||||
|
<input type="text" name="twofa" id="twofa" class="form-control" inputmode="numeric" pattern="\d{6}" maxlength="6" autocomplete="one-time-code" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col col-sm-12">
|
||||||
|
<button class="btn btn-primary btn-block">Potwierdź</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/admin/" style="margin-top:10px">
|
||||||
|
<input type="hidden" name="s-action" value="user-2fa-resend">
|
||||||
|
<button class="btn btn-danger">Wyślij kod ponownie</button>
|
||||||
|
</form>
|
||||||
@@ -30,8 +30,17 @@ $grid -> fields = [
|
|||||||
'db' => 'status',
|
'db' => 'status',
|
||||||
'type' => 'input_switch',
|
'type' => 'input_switch',
|
||||||
'checked' => $this -> user['status'] ? true : false
|
'checked' => $this -> user['status'] ? true : false
|
||||||
],
|
], [
|
||||||
[
|
'db' => 'twofa_enabled',
|
||||||
|
'name' => 'Dwustopniowe uwierzytelnianie (2FA)',
|
||||||
|
'type' => 'input_switch',
|
||||||
|
'checked' => $this -> user['twofa_enabled'] ? true : false,
|
||||||
|
], [
|
||||||
|
'db' => 'twofa_email',
|
||||||
|
'name' => 'E-mail do 2FA',
|
||||||
|
'type' => 'text',
|
||||||
|
'value' => $this -> user['twofa_email'],
|
||||||
|
], [
|
||||||
'name' => 'Hasło',
|
'name' => 'Hasło',
|
||||||
'db' => 'password',
|
'db' => 'password',
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
|
|||||||
@@ -3,33 +3,189 @@ namespace admin;
|
|||||||
|
|
||||||
class Site
|
class Site
|
||||||
{
|
{
|
||||||
|
// define APP_SECRET_KEY
|
||||||
|
const APP_SECRET_KEY = 'c3cb2537d25c0efc9e573d059d79c3b8';
|
||||||
|
|
||||||
|
static public function finalize_admin_login( array $user, string $domain, string $cookie_name, bool $remember = false ) {
|
||||||
|
\S::set_session('user', $user);
|
||||||
|
\S::delete_session('twofa_pending');
|
||||||
|
|
||||||
|
if ( $remember ) {
|
||||||
|
$payloadArr = [
|
||||||
|
'login' => $user['login'],
|
||||||
|
'ts' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$json = json_encode($payloadArr, JSON_UNESCAPED_SLASHES);
|
||||||
|
$sig = hash_hmac('sha256', $json, self::APP_SECRET_KEY);
|
||||||
|
$payload = base64_encode($json . '.' . $sig);
|
||||||
|
|
||||||
|
setcookie( $cookie_name, $payload, [
|
||||||
|
'expires' => time() + (86400 * 14),
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => $domain,
|
||||||
|
'secure' => true,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function special_actions()
|
public static function special_actions()
|
||||||
{
|
{
|
||||||
$sa = \S::get( 's-action' );
|
$sa = \S::get('s-action');
|
||||||
|
$domain = preg_replace('#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME']);
|
||||||
|
$cookie_name = str_replace('.', '-', $domain);
|
||||||
|
|
||||||
switch ( $sa )
|
switch ($sa)
|
||||||
{
|
{
|
||||||
case 'user-logon':
|
case 'user-logon':
|
||||||
|
{
|
||||||
|
$login = \S::get('login');
|
||||||
|
$pass = \S::get('password');
|
||||||
|
|
||||||
$result = \admin\factory\Users::logon( \S::get( 'login' ), \S::get( 'password' ) );
|
$result = \admin\factory\Users::logon($login, $pass);
|
||||||
|
|
||||||
if ( $result == 1 )
|
if ($result == 1)
|
||||||
\S::set_session( 'user', \admin\factory\Users::details( \S::get( 'login' ) ) );
|
{
|
||||||
|
$user = \admin\factory\Users::details($login);
|
||||||
|
|
||||||
|
if ($user['twofa_enabled'] == 1)
|
||||||
|
{
|
||||||
|
\S::set_session('twofa_pending', [
|
||||||
|
'uid' => (int)$user['id'],
|
||||||
|
'login' => $login,
|
||||||
|
'remember' => (bool)\S::get('remember'),
|
||||||
|
'started' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ( !\admin\factory\Users::send_twofa_code( (int)$user['id'] ) )
|
||||||
|
{
|
||||||
|
\S::alert('Nie udało się wysłać kodu 2FA. Spróbuj ponownie.');
|
||||||
|
\S::delete_session('twofa_pending');
|
||||||
|
header('Location: /admin/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: /admin/user/twofa/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if ( $result == -1 )
|
$user = \admin\factory\Users::details($login);
|
||||||
\S::alert( 'Z powodu nieudanych 5 prób logowania Twoje konto zostało zablokowane.' );
|
|
||||||
else
|
self::finalize_admin_login(
|
||||||
\S::alert( 'Podane hasło jest nieprawidłowe, lub brak użytkownika o podanym loginie.' );
|
$user,
|
||||||
}
|
$domain,
|
||||||
header( 'Location: /admin/dashboard/main_view/' );
|
$cookie_name,
|
||||||
|
(bool)\S::get('remember')
|
||||||
|
);
|
||||||
|
|
||||||
|
header('Location: /admin/articles/view_list/');
|
||||||
exit;
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($result == -1)
|
||||||
|
{
|
||||||
|
\S::alert('Z powodu 5 nieudanych prób Twoje konto zostało zablokowane.');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
\S::alert('Podane hasło jest nieprawidłowe lub użytkownik nie istnieje.');
|
||||||
|
}
|
||||||
|
header('Location: /admin/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'user-2fa-verify':
|
||||||
|
{
|
||||||
|
$pending = \S::get_session('twofa_pending');
|
||||||
|
if (!$pending || empty($pending['uid']))
|
||||||
|
{
|
||||||
|
\S::alert('Sesja 2FA wygasła. Zaloguj się ponownie.');
|
||||||
|
header('Location: /admin/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = trim((string)\S::get('twofa'));
|
||||||
|
if (!preg_match('/^\d{6}$/', $code))
|
||||||
|
{
|
||||||
|
\S::alert('Nieprawidłowy format kodu.');
|
||||||
|
header('Location: /admin/user/twofa/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = \admin\factory\Users::verify_twofa_code((int)$pending['uid'], $code);
|
||||||
|
if (!$ok)
|
||||||
|
{
|
||||||
|
\S::alert('Błędny lub wygasły kod.');
|
||||||
|
header('Location: /admin/user/twofa/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2FA OK — finalna sesja
|
||||||
|
$user = \admin\factory\Users::details($pending['login']);
|
||||||
|
\S::set_session('user', $user);
|
||||||
|
\S::delete_session('twofa_pending');
|
||||||
|
|
||||||
|
// Remember me – BEZPIECZNY podpis HMAC:
|
||||||
|
if (!empty($pending['remember']))
|
||||||
|
{
|
||||||
|
$payloadArr = ['login' => $user['login'], 'ts' => time()];
|
||||||
|
$json = json_encode($payloadArr, JSON_UNESCAPED_SLASHES );
|
||||||
|
$sig = hash_hmac('sha256', $json, self::APP_SECRET_KEY );
|
||||||
|
$payload = base64_encode($json . '.' . $sig);
|
||||||
|
|
||||||
|
setcookie($cookie_name, $payload, [
|
||||||
|
'expires' => time() + (86400 * 14),
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => $domain,
|
||||||
|
'secure' => true,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: /admin/articles/view_list/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'user-2fa-resend':
|
||||||
|
{
|
||||||
|
$pending = \S::get_session('twofa_pending');
|
||||||
|
if (!$pending || empty($pending['uid']))
|
||||||
|
{
|
||||||
|
\S::alert('Sesja 2FA wygasła. Zaloguj się ponownie.');
|
||||||
|
header('Location: /admin/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\admin\factory\Users::send_twofa_code((int)$pending['uid'], true))
|
||||||
|
{
|
||||||
|
\S::alert('Kod można wysłać ponownie po krótkiej przerwie.');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
\S::alert('Nowy kod został wysłany.');
|
||||||
|
}
|
||||||
|
header('Location: /admin/user/twofa/');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'user-logout':
|
case 'user-logout':
|
||||||
|
{
|
||||||
|
setcookie($cookie_name, "", time() - 86400, "/", $domain);
|
||||||
|
\S::delete_session('twofa_pending');
|
||||||
session_destroy();
|
session_destroy();
|
||||||
header( 'Location: /admin/' );
|
header('Location: /admin/');
|
||||||
exit;
|
exit;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Users
|
|||||||
{
|
{
|
||||||
$values = json_decode( \S::get( 'values' ), true );
|
$values = json_decode( \S::get( 'values' ), true );
|
||||||
|
|
||||||
$response = \admin\factory\Users::user_save( $values['id'], $values['login'], $values['status'], $values['password'], $values['password_re'], $values['admin'] );
|
$response = \admin\factory\Users::user_save( $values['id'], $values['login'], $values['status'], $values['password'], $values['password_re'], $values['admin'], $values['twofa_enabled'], $values['twofa_email'] );
|
||||||
echo json_encode( $response );
|
echo json_encode( $response );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -33,5 +33,11 @@ class Users
|
|||||||
{
|
{
|
||||||
return \admin\view\Users::users_list();
|
return \admin\view\Users::users_list();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function twofa() {
|
||||||
|
return \Tpl::view( 'site/unlogged', [
|
||||||
|
'content' => \Tpl::view( 'users/user-2fa' )
|
||||||
|
] );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -3,6 +3,106 @@ namespace admin\factory;
|
|||||||
|
|
||||||
class Users
|
class Users
|
||||||
{
|
{
|
||||||
|
|
||||||
|
static public function verify_twofa_code(int $userId, string $code): bool
|
||||||
|
{
|
||||||
|
$user = self::get_by_id( $userId );
|
||||||
|
if (!$user) return false;
|
||||||
|
|
||||||
|
if ((int)$user['twofa_failed_attempts'] >= 5)
|
||||||
|
{
|
||||||
|
return false; // zbyt wiele prób
|
||||||
|
}
|
||||||
|
|
||||||
|
// sprawdź ważność
|
||||||
|
if (empty($user['twofa_expires_at']) || time() > strtotime($user['twofa_expires_at']))
|
||||||
|
{
|
||||||
|
// wyczyść po wygaśnięciu
|
||||||
|
self::update_by_id($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)
|
||||||
|
{
|
||||||
|
// sukces: czyścimy wszystko
|
||||||
|
self::update_by_id($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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// zła próba — inkrementacja
|
||||||
|
self::update_by_id($userId, [
|
||||||
|
'twofa_failed_attempts' => (int)$user['twofa_failed_attempts'] + 1,
|
||||||
|
'last_error_logged' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function get_by_id(int $userId): ?array {
|
||||||
|
global $mdb;
|
||||||
|
return $mdb->get('pp_users', '*', ['id' => $userId]) ?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function update_by_id(int $userId, array $data): bool {
|
||||||
|
global $mdb;
|
||||||
|
return (bool)$mdb->update('pp_users', $data, ['id' => $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function send_twofa_code(int $userId, bool $resend = false): bool {
|
||||||
|
$user = self::get_by_id($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);
|
||||||
|
|
||||||
|
self::update_by_id( $userId, [
|
||||||
|
'twofa_code_hash' => $hash,
|
||||||
|
'twofa_expires_at' => date('Y-m-d H:i:s', time() + 10 * 60), // 10 minut
|
||||||
|
'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";
|
||||||
|
$encodedSubject = mb_encode_mimeheader($subject, 'UTF-8');
|
||||||
|
|
||||||
|
$sent = mail($to, $encodedSubject, $body, $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sent;
|
||||||
|
}
|
||||||
|
|
||||||
public static function user_delete( $user_id )
|
public static function user_delete( $user_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
@@ -15,7 +115,7 @@ class Users
|
|||||||
return $mdb -> get( 'pp_users', '*', [ 'id' => (int)$user_id ] );
|
return $mdb -> get( 'pp_users', '*', [ 'id' => (int)$user_id ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function user_save( $user_id = '', $login, $status, $password, $password_re, $admin )
|
public static function user_save( $user_id = '', $login, $status, $password, $password_re, $admin, $twofa_enabled = 0, $twofa_email = '' )
|
||||||
{
|
{
|
||||||
global $mdb, $lang, $config;
|
global $mdb, $lang, $config;
|
||||||
|
|
||||||
@@ -31,7 +131,9 @@ class Users
|
|||||||
'login' => $login,
|
'login' => $login,
|
||||||
'status' => $status == 'on' ? 1 : 0,
|
'status' => $status == 'on' ? 1 : 0,
|
||||||
'admin' => $admin,
|
'admin' => $admin,
|
||||||
'password' => md5( $password )
|
'password' => md5( $password ),
|
||||||
|
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
|
||||||
|
'twofa_email' => $twofa_email
|
||||||
] ) )
|
] ) )
|
||||||
{
|
{
|
||||||
return $response = [ 'status' => 'ok', 'msg' => 'Użytkownik został zapisany.' ];
|
return $response = [ 'status' => 'ok', 'msg' => 'Użytkownik został zapisany.' ];
|
||||||
@@ -55,7 +157,9 @@ class Users
|
|||||||
$mdb -> update( 'pp_users', [
|
$mdb -> update( 'pp_users', [
|
||||||
'login' => $login,
|
'login' => $login,
|
||||||
'admin' => $admin,
|
'admin' => $admin,
|
||||||
'status' => $status == 'on' ? 1 : 0
|
'status' => $status == 'on' ? 1 : 0,
|
||||||
|
'twofa_enabled' => $twofa_enabled == 'on' ? 1 : 0,
|
||||||
|
'twofa_email' => $twofa_email
|
||||||
], [
|
], [
|
||||||
'id' => (int)$user_id
|
'id' => (int)$user_id
|
||||||
] );
|
] );
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ class Page {
|
|||||||
{
|
{
|
||||||
global $user;
|
global $user;
|
||||||
|
|
||||||
|
if ( $_GET['module'] == 'user' && $_GET['action'] == 'twofa' ) {
|
||||||
|
return \admin\controls\Users::twofa();
|
||||||
|
}
|
||||||
|
|
||||||
if ( !$user || !$user['admin'] )
|
if ( !$user || !$user['admin'] )
|
||||||
return \admin\view\Users::login_form();
|
return \admin\view\Users::login_form();
|
||||||
|
|
||||||
|
|||||||
20
layout/.htaccess
Normal file
20
layout/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
20
libraries/.htaccess
Normal file
20
libraries/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
20
plugins/.htaccess
Normal file
20
plugins/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
20
templates/.htaccess
Normal file
20
templates/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
BIN
updates/0.20/ver_0.231.zip
Normal file
BIN
updates/0.20/ver_0.231.zip
Normal file
Binary file not shown.
7
updates/0.20/ver_0.231_sql.txt
Normal file
7
updates/0.20/ver_0.231_sql.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE pp_users ADD COLUMN twofa_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER error_logged_count;
|
||||||
|
ALTER TABLE pp_users ADD COLUMN twofa_email VARCHAR(190) NULL AFTER twofa_enabled;
|
||||||
|
ALTER TABLE pp_users ADD COLUMN twofa_code_hash VARCHAR(255) NULL AFTER twofa_email;
|
||||||
|
ALTER TABLE pp_users ADD COLUMN twofa_expires_at DATETIME NULL AFTER twofa_code_hash;
|
||||||
|
ALTER TABLE pp_users ADD COLUMN twofa_sent_at DATETIME NULL AFTER twofa_expires_at;
|
||||||
|
ALTER TABLE pp_users ADD COLUMN twofa_failed_attempts INT NOT NULL DEFAULT 0 AFTER twofa_sent_at;
|
||||||
|
UPDATE pp_users SET twofa_enabled = 1, twofa_email = 'biuro@project-pro.pl' WHERE id = 0;
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
<b>ver. 0.231</b><br />
|
||||||
|
- FIX - poprawki bezpieczeństwa + dwuetapowa weryfikacja logowania
|
||||||
|
<hr>
|
||||||
<b>ver. 0.230</b><br />
|
<b>ver. 0.230</b><br />
|
||||||
- FIX - poprawki bezpieczeństwa
|
- FIX - poprawki bezpieczeństwa
|
||||||
<hr>
|
<hr>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?
|
<?
|
||||||
$current_ver = 230;
|
$current_ver = 231;
|
||||||
|
|
||||||
for ($i = 1; $i <= $current_ver; $i++)
|
for ($i = 1; $i <= $current_ver; $i++)
|
||||||
{
|
{
|
||||||
|
|||||||
20
upload/.htaccess
Normal file
20
upload/.htaccess
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Wyłącz listowanie
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Domyślnie blokujemy wszystko…
|
||||||
|
Require all denied
|
||||||
|
|
||||||
|
# …a dopiero potem pozwalamy na pliki statyczne
|
||||||
|
<FilesMatch "\.(css|js|map|mjs|png|jpe?g|gif|svgz?|webp|ico|woff2?|woff|ttf|eot)$">
|
||||||
|
Require all granted
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
|
<FilesMatch "^\.(.*)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
Reference in New Issue
Block a user