dev #1

Merged
jester merged 7 commits from dev into main 2026-01-19 16:51:21 +03:00
32 changed files with 498 additions and 841 deletions

View File

@@ -1,52 +0,0 @@
# Auth Form (PHP drop-in)
Современная форма авторизации с проверкой доступа к сайту через MariaDB.
Файлы можно копировать прямо в папку существующего сайта.
## Требования
- PHP 7.4+ (mysqli, sessions)
- MariaDB
## Установка
1. Создайте базу и таблицы:
- Запустите SQL из `db/schema.sql`
2. Заполните подключение к БД:
- Отредактируйте `auth/config.php`
3. Добавьте пользователя и доступ:
- `INSERT INTO users (login, password_hash) VALUES ('demo', '<HASH>');`
- `INSERT INTO user_access (user_id, site_key) VALUES (1, 'example.com');`
## Хэш пароля
Создать bcrypt-хэш можно так:
```php
<?php
echo password_hash("yourPassword", PASSWORD_BCRYPT);
```
## Интеграция в сайт (NAS)
1. Скопируйте папку `auth` в корень сайта.
2. На защищаемых HTML-страницах (кроме главной) добавьте:
```html
<meta name="site-key" content="example.com" />
<script src="/auth/guard.js"></script>
```
3. Главную страницу не защищайте — вход находится в `/auth/login.html`.
4. Если страницы могут быть PHP, используйте серверную защиту:
```php
<?php
define("AUTH_SITE_KEY", "example.com");
require __DIR__ . "/auth/guard.php";
```
## Лимиты и логирование
- Ограничение попыток: 10 запросов за 5 минут на IP
- Логи авторизации: `auth/logs/auth.log` (JSONL-строки)

16
api/check_auth.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once '../config/session.php';
if (isset($_SESSION['user_id'])) {
echo json_encode([
'isLoggedIn' => true,
'user' => [
'id' => $_SESSION['user_id'],
'username' => $_SESSION['username']
]
]);
} else {
echo json_encode(['isLoggedIn' => false]);
}
?>

64
api/login.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
header('Content-Type: application/json; charset=utf-8');
require_once '../config/db.php';
require_once '../config/session.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Метод не поддерживается']);
exit;
}
$data = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Некорректные данные']);
exit;
}
$login = trim($data['username'] ?? '');
$password = $data['password'] ?? '';
$siteAlias = trim($data['site_alias'] ?? '');
if ($siteAlias === '') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Не указан сайт']);
exit;
}
// Поиск пользователя по username
// $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt = $pdo->prepare("SELECT id, username, password_hash, ok5, o7, o10m, o10a, webp FROM users WHERE username = ?");
$stmt->execute([$login]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password_hash'])) {
$allowedAliases = ['ok5', 'o7', 'o10m', 'o10a', 'webp'];
if (!in_array($siteAlias, $allowedAliases, true)) {
echo json_encode(['success' => false, 'message' => 'Неизвестный сайт']);
exit;
}
if ((int)$user[$siteAlias] !== 1) {
echo json_encode(['success' => false, 'message' => 'Нет доступа к сайту']);
exit;
}
// Успешная авторизация
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
echo json_encode([
'success' => true,
'message' => 'Вход выполнен успешно',
'user' => [
'id' => $user['id'],
'username' => $user['username'],
]
]);
} else {
echo json_encode(['success' => false, 'message' => 'Неверные учетные данные']);
}
?>

18
api/logout.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once '../config/session.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Метод не поддерживается']);
exit;
}
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
echo json_encode(['success' => true]);
?>

View File

@@ -1,44 +0,0 @@
<?php
require __DIR__ . "/../util.php";
require __DIR__ . "/../db.php";
require __DIR__ . "/../logger.php";
session_start();
$siteKey = auth_get_site_key($_GET);
$clientIp = auth_get_client_ip();
if (!auth_is_valid_site_key($siteKey)) {
auth_log_event([
"ip" => $clientIp,
"siteKey" => $siteKey,
"status" => "invalid_payload",
]);
auth_json_response(400, ["ok" => false, "message" => "Неверные данные."]);
}
$userId = (int) ($_SESSION["auth_user_id"] ?? 0);
if ($userId <= 0) {
auth_json_response(401, ["ok" => false, "message" => "Требуется вход."]);
}
$db = auth_get_db();
$stmt = $db->prepare("SELECT 1 FROM user_access WHERE user_id = ? AND site_key = ? LIMIT 1");
$stmt->bind_param("is", $userId, $siteKey);
$stmt->execute();
$result = $stmt->get_result();
$hasAccess = $result && $result->num_rows > 0;
$stmt->close();
if (!$hasAccess) {
auth_log_event([
"ip" => $clientIp,
"userId" => $userId,
"siteKey" => $siteKey,
"status" => "access_denied",
]);
auth_json_response(403, ["ok" => false, "message" => "Нет доступа."]);
}
auth_json_response(200, ["ok" => true]);

View File

@@ -1,87 +0,0 @@
<?php
require __DIR__ . "/../util.php";
require __DIR__ . "/../db.php";
require __DIR__ . "/../logger.php";
require __DIR__ . "/../rate_limit.php";
session_start();
$data = array_merge($_POST, auth_read_json());
$login = trim((string) ($data["login"] ?? ""));
$password = (string) ($data["password"] ?? "");
$siteKey = auth_get_site_key($data);
$clientIp = auth_get_client_ip();
if (!auth_is_valid_login($login) || !auth_is_valid_password($password) || !auth_is_valid_site_key($siteKey)) {
auth_log_event([
"ip" => $clientIp,
"login" => $login,
"siteKey" => $siteKey,
"status" => "invalid_payload",
]);
auth_json_response(400, ["ok" => false, "message" => "Неверные данные."]);
}
$rate = auth_check_rate_limit($clientIp);
if ($rate["limited"]) {
auth_log_event([
"ip" => $clientIp,
"login" => $login,
"siteKey" => $siteKey,
"status" => "rate_limited",
]);
header("Retry-After: " . (string) $rate["retry_after"]);
auth_json_response(429, ["ok" => false, "message" => "Слишком много попыток. Попробуйте позже."]);
}
$db = auth_get_db();
$stmt = $db->prepare("SELECT id, password_hash FROM users WHERE login = ? LIMIT 1");
$stmt->bind_param("s", $login);
$stmt->execute();
$result = $stmt->get_result();
$row = $result ? $result->fetch_assoc() : null;
$stmt->close();
$hash = $row["password_hash"] ?? password_hash("invalid_password", PASSWORD_BCRYPT);
$passwordOk = password_verify($password, $hash);
if (!$row || !$passwordOk) {
auth_log_event([
"ip" => $clientIp,
"login" => $login,
"siteKey" => $siteKey,
"status" => "invalid_credentials",
]);
auth_json_response(401, ["ok" => false, "message" => "Неверный логин или пароль."]);
}
$stmt = $db->prepare("SELECT 1 FROM user_access WHERE user_id = ? AND site_key = ? LIMIT 1");
$stmt->bind_param("is", $row["id"], $siteKey);
$stmt->execute();
$accessResult = $stmt->get_result();
$hasAccess = $accessResult && $accessResult->num_rows > 0;
$stmt->close();
if (!$hasAccess) {
auth_log_event([
"ip" => $clientIp,
"login" => $login,
"siteKey" => $siteKey,
"status" => "access_denied",
]);
auth_json_response(403, ["ok" => false, "message" => "Нет доступа к этому сайту."]);
}
$_SESSION["auth_user_id"] = (int) $row["id"];
$_SESSION["auth_login"] = $login;
$_SESSION["auth_time"] = time();
auth_log_event([
"ip" => $clientIp,
"login" => $login,
"siteKey" => $siteKey,
"status" => "success",
]);
auth_json_response(200, ["ok" => true]);

View File

@@ -1,19 +0,0 @@
<?php
return [
"db" => [
"host" => "db",
"port" => 3306,
"user" => "root",
"password" => "rootpass",
"name" => "auth_db",
],
"rate_limit" => [
"window_seconds" => 300,
"max_attempts" => 10,
],
"logging" => [
"dir" => __DIR__ . "/logs",
"file" => __DIR__ . "/logs/auth.log",
],
];

View File

@@ -1,19 +0,0 @@
<?php
return [
"db" => [
"host" => "127.0.0.1",
"port" => 3306,
"user" => "root",
"password" => "",
"name" => "auth_db",
],
"rate_limit" => [
"window_seconds" => 300,
"max_attempts" => 10,
],
"logging" => [
"dir" => __DIR__ . "/logs",
"file" => __DIR__ . "/logs/auth.log",
],
];

View File

@@ -1,17 +0,0 @@
<?php
function auth_get_db(): mysqli
{
$config = require __DIR__ . "/config.php";
$db = $config["db"];
$mysqli = new mysqli($db["host"], $db["user"], $db["password"], $db["name"], $db["port"]);
if ($mysqli->connect_errno) {
http_response_code(500);
echo json_encode(["ok" => false, "message" => "Ошибка сервера."]);
exit;
}
$mysqli->set_charset("utf8mb4");
return $mysqli;
}

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="site-key" content="localhost" />
<title>Демо защищенной страницы</title>
<link rel="stylesheet" href="./styles.css" />
<script src="/auth/guard.js"></script>
</head>
<body>
<main class="page">
<section class="card" aria-labelledby="demo-title">
<div class="card__header">
<h1 id="demo-title">Демо защищенной страницы</h1>
<p class="card__subtitle">
Если вы видите эту страницу, доступ разрешен.
</p>
</div>
<a class="button" href="/auth/login.html">Перейти к форме входа</a>
</section>
</main>
</body>
</html>

View File

@@ -1,35 +0,0 @@
function authGetSiteKey() {
const meta = document.querySelector('meta[name="site-key"]');
if (meta && meta.content) return meta.content.trim();
const bodyKey = document.body?.dataset?.siteKey;
if (bodyKey) return bodyKey.trim();
return window.location.hostname || "unknown";
}
function authBuildLoginUrl(siteKey) {
const current = window.location.pathname + window.location.search;
const params = new URLSearchParams({
siteKey,
redirect: current,
});
return `/auth/login.html?${params.toString()}`;
}
async function authCheckAccess() {
const siteKey = authGetSiteKey();
try {
const response = await fetch(`/auth/api/check.php?siteKey=${encodeURIComponent(siteKey)}`, {
method: "GET",
headers: { "Accept": "application/json" },
credentials: "same-origin",
});
if (response.ok) return;
window.location.href = authBuildLoginUrl(siteKey);
} catch (error) {
window.location.href = authBuildLoginUrl(siteKey);
}
}
authCheckAccess();

View File

@@ -1,46 +0,0 @@
<?php
require __DIR__ . "/util.php";
require __DIR__ . "/db.php";
require __DIR__ . "/logger.php";
session_start();
$siteKey = defined("AUTH_SITE_KEY") ? AUTH_SITE_KEY : ($_SERVER["HTTP_HOST"] ?? "unknown");
$siteKey = is_string($siteKey) ? trim($siteKey) : "unknown";
if (!auth_is_valid_site_key($siteKey)) {
auth_log_event([
"ip" => auth_get_client_ip(),
"siteKey" => $siteKey,
"status" => "invalid_payload",
]);
header("Location: /auth/login.html");
exit;
}
$userId = (int) ($_SESSION["auth_user_id"] ?? 0);
if ($userId <= 0) {
$redirect = $_SERVER["REQUEST_URI"] ?? "/";
header("Location: /auth/login.html?siteKey=" . urlencode($siteKey) . "&redirect=" . urlencode($redirect));
exit;
}
$db = auth_get_db();
$stmt = $db->prepare("SELECT 1 FROM user_access WHERE user_id = ? AND site_key = ? LIMIT 1");
$stmt->bind_param("is", $userId, $siteKey);
$stmt->execute();
$result = $stmt->get_result();
$hasAccess = $result && $result->num_rows > 0;
$stmt->close();
if (!$hasAccess) {
auth_log_event([
"ip" => auth_get_client_ip(),
"userId" => $userId,
"siteKey" => $siteKey,
"status" => "access_denied",
]);
header("Location: /auth/login.html?siteKey=" . urlencode($siteKey));
exit;
}

View File

@@ -1,30 +0,0 @@
<?php
function auth_log_event(array $entry): void
{
$config = require __DIR__ . "/config.php";
$dir = $config["logging"]["dir"];
$file = $config["logging"]["file"];
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
$entry["ts"] = $entry["ts"] ?? gmdate("c");
$line = json_encode($entry, JSON_UNESCAPED_UNICODE);
if ($line === false) {
return;
}
$fh = @fopen($file, "ab");
if ($fh === false) {
return;
}
if (flock($fh, LOCK_EX)) {
fwrite($fh, $line . PHP_EOL);
flock($fh, LOCK_UN);
}
fclose($fh);
}

View File

@@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="site-key" content="example.com" />
<title>Вход</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="page">
<section class="card" aria-labelledby="login-title">
<div class="card__header">
<h1 id="login-title">Добро пожаловать</h1>
<p class="card__subtitle">Войдите, чтобы продолжить</p>
</div>
<form class="form" id="login-form" novalidate>
<label class="field">
<span class="field__label">Логин</span>
<input
class="field__input"
type="text"
name="login"
autocomplete="username"
minlength="3"
maxlength="64"
required
/>
</label>
<label class="field">
<span class="field__label">Пароль</span>
<input
class="field__input"
type="password"
name="password"
autocomplete="current-password"
minlength="8"
maxlength="128"
required
/>
</label>
<button class="button" type="submit">
Войти
</button>
<div class="form__message" id="form-message" role="status" aria-live="polite"></div>
</form>
</section>
</main>
<script src="./login.js"></script>
</body>
</html>

View File

@@ -1,64 +0,0 @@
const form = document.getElementById("login-form");
const messageEl = document.getElementById("form-message");
function getSiteKey() {
const params = new URLSearchParams(window.location.search);
const urlKey = params.get("siteKey");
if (urlKey) return urlKey.trim();
const meta = document.querySelector('meta[name="site-key"]');
if (meta && meta.content) return meta.content.trim();
return window.location.hostname || "unknown";
}
function getRedirectUrl() {
const params = new URLSearchParams(window.location.search);
return params.get("redirect") || "/";
}
function setMessage(text, type) {
messageEl.textContent = text;
messageEl.classList.remove("form__message--error", "form__message--success");
if (type === "error") messageEl.classList.add("form__message--error");
if (type === "success") messageEl.classList.add("form__message--success");
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const formData = new FormData(form);
const login = String(formData.get("login") || "").trim();
const password = String(formData.get("password") || "");
const siteKey = getSiteKey();
if (!login || !password) {
setMessage("Введите логин и пароль.", "error");
return;
}
setMessage("Проверяем данные...", "");
const submitButton = form.querySelector("button[type='submit']");
submitButton.disabled = true;
try {
const response = await fetch("./api/login.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ login, password, siteKey }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
setMessage(data?.message || "Ошибка входа.", "error");
return;
}
setMessage("Доступ разрешен.", "success");
const redirect = getRedirectUrl();
setTimeout(() => {
window.location.href = redirect;
}, 400);
} catch (error) {
setMessage("Сеть недоступна.", "error");
} finally {
submitButton.disabled = false;
}
});

View File

@@ -1,55 +0,0 @@
<?php
function auth_check_rate_limit(string $key): array
{
$config = require __DIR__ . "/config.php";
$window = (int) $config["rate_limit"]["window_seconds"];
$max = (int) $config["rate_limit"]["max_attempts"];
$dir = __DIR__ . "/ratelimit";
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
$safeKey = hash("sha256", $key);
$file = $dir . "/" . $safeKey . ".json";
$now = time();
$data = [
"count" => 1,
"reset_at" => $now + $window,
];
if (is_file($file)) {
$raw = @file_get_contents($file);
$decoded = $raw ? json_decode($raw, true) : null;
if (is_array($decoded) && isset($decoded["count"], $decoded["reset_at"])) {
if ($now <= (int) $decoded["reset_at"]) {
$decoded["count"] = (int) $decoded["count"] + 1;
$data = $decoded;
}
}
}
$limited = $data["count"] > $max;
$retryAfter = max(0, $data["reset_at"] - $now);
$payload = json_encode($data);
if ($payload !== false) {
$fh = @fopen($file, "cb+");
if ($fh !== false) {
if (flock($fh, LOCK_EX)) {
ftruncate($fh, 0);
fwrite($fh, $payload);
fflush($fh);
flock($fh, LOCK_UN);
}
fclose($fh);
}
}
return [
"limited" => $limited,
"retry_after" => $retryAfter,
];
}

View File

@@ -1,117 +0,0 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
color: #0b1320;
background: radial-gradient(circle at top, #f0f4ff 0%, #e4ecff 45%, #dfe8ff 100%);
min-height: 100vh;
}
.page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 16px;
}
.card {
width: min(420px, 100%);
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(16px);
border-radius: 20px;
padding: 32px;
box-shadow: 0 20px 60px rgba(25, 42, 80, 0.15);
border: 1px solid rgba(255, 255, 255, 0.6);
}
.card__header {
margin-bottom: 24px;
}
.card__header h1 {
margin: 0 0 8px;
font-size: 26px;
font-weight: 700;
}
.card__subtitle {
margin: 0;
color: #51607a;
font-size: 14px;
}
.form {
display: grid;
gap: 16px;
}
.field {
display: grid;
gap: 8px;
}
.field__label {
font-size: 13px;
color: #4c5a73;
}
.field__input {
width: 100%;
border-radius: 12px;
border: 1px solid #d6def2;
padding: 12px 14px;
font-size: 15px;
transition: border 0.2s ease, box-shadow 0.2s ease;
}
.field__input:focus {
outline: none;
border-color: #6f87ff;
box-shadow: 0 0 0 4px rgba(111, 135, 255, 0.2);
}
.button {
display: inline-block;
text-decoration: none;
text-align: center;
border: none;
border-radius: 12px;
padding: 12px 16px;
font-size: 15px;
font-weight: 600;
color: #fff;
background: linear-gradient(120deg, #5b7cfa, #7b5bfa);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(91, 124, 250, 0.25);
}
.button:disabled {
cursor: not-allowed;
opacity: 0.7;
box-shadow: none;
}
.form__message {
min-height: 20px;
font-size: 13px;
color: #4c5a73;
}
.form__message--error {
color: #b91c1c;
}
.form__message--success {
color: #166534;
}

View File

@@ -1,53 +0,0 @@
<?php
function auth_get_client_ip(): string
{
$forwarded = $_SERVER["HTTP_X_FORWARDED_FOR"] ?? "";
if ($forwarded) {
$parts = explode(",", $forwarded);
return trim($parts[0]);
}
return $_SERVER["REMOTE_ADDR"] ?? "unknown";
}
function auth_read_json(): array
{
$raw = file_get_contents("php://input");
if (!$raw) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
function auth_json_response(int $status, array $payload): void
{
http_response_code($status);
header("Content-Type: application/json; charset=utf-8");
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
exit;
}
function auth_get_site_key(array $data): string
{
$siteKey = $data["siteKey"] ?? $_GET["siteKey"] ?? "";
return is_string($siteKey) ? trim($siteKey) : "";
}
function auth_is_valid_login(string $login): bool
{
$len = mb_strlen($login, "UTF-8");
return $len >= 3 && $len <= 64;
}
function auth_is_valid_password(string $password): bool
{
$len = mb_strlen($password, "UTF-8");
return $len >= 8 && $len <= 128;
}
function auth_is_valid_site_key(string $siteKey): bool
{
$len = mb_strlen($siteKey, "UTF-8");
return $len >= 3 && $len <= 255;
}

16
config/db.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
$host = 'localhost';
$dbname = 'users';
$username = 'root';
$password = 'RUS4t8pm!@#';
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
die("Ошибка подключения: " . $e->getMessage());
}
?>

15
config/session.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
$isSecure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
ini_set('session.use_strict_mode', '1');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $isSecure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
?>

98
css/auth.css Normal file
View File

@@ -0,0 +1,98 @@
.btn-primary {
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 8px;
background: #3b82f6;
color: #fff;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #2563eb;
}
:root {
color-scheme: light;
--bg: #f5f7fb;
--card: #ffffff;
--text: #1b1f2a;
--muted: #6b7280;
--border: #e5e7eb;
--accent: #3b82f6;
--accent-dark: #2563eb;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", Arial, sans-serif;
background: linear-gradient(135deg, #eef2ff, #f8fafc);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
}
.card {
width: 100%;
max-width: 520px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 42px;
padding: 24px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
}
h2 {
margin: 0 0 16px;
font-size: 24px;
}
.actions {
display: flex;
justify-content: flex-end;
}
.subtitle {
margin: 0 0 20px;
color: var(--muted);
font-size: 14px;
}
.field {
margin-bottom: 14px;
}
label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
button {
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 8px;
background: var(--accent);
color: #fff;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}
button:hover { background: var(--accent-dark); }
.helper {
margin-top: 14px;
font-size: 12px;
color: var(--muted);
text-align: center;
}

View File

@@ -1,32 +0,0 @@
CREATE DATABASE IF NOT EXISTS auth_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE auth_db;
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
login VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS user_access (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
site_key VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY unique_access (user_id, site_key),
CONSTRAINT fk_user_access_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
-- Пример:
-- INSERT INTO users (login, password_hash)
-- VALUES ('demo', '$2a$10$YOUR_BCRYPT_HASH_HERE');
--
-- INSERT INTO user_access (user_id, site_key)
-- VALUES (1, 'example.com');

View File

@@ -1,7 +0,0 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli
RUN a2enmod rewrite headers
WORKDIR /var/www/html

View File

@@ -1,56 +0,0 @@
# Docker инструкция (Windows)
Все файлы для Docker лежат в `docker-pack/` и не трогают текущее решение.
## Требования
- Docker Desktop (включите WSL2 backend)
## Быстрый старт
1. Перейдите в каталог:
- `cd d:\work\code\auth\docker-pack`
2. Соберите и запустите контейнеры:
- `docker compose up -d --build`
3. Откройте в браузере:
- `http://localhost:8080/auth/login.html`
## Настройка БД
По умолчанию MariaDB поднимается с:
- БД: `auth_db`
- root пароль: `rootpass`
Схема создается автоматически из `../db/schema.sql`.
Пользователь `demo` добавляется автоматически из `seed.sql`.
### Тестовые данные
По умолчанию создается пользователь:
- логин: `demo`
- пароль: `demo12345`
- site_key: `localhost`
Если нужно изменить — отредактируйте `seed.sql` и пересоздайте контейнеры:
- `docker compose down -v`
- `docker compose up -d --build`
## Важно про конфиг PHP
В `auth/config.php` укажите параметры подключения:
- host: `db`
- port: `3306`
- user: `root`
- password: `rootpass`
- name: `auth_db`
Если хотите оставить `auth/config.php` для локальной сети, заведите отдельную копию
только для Docker и подмените ее через volume в `docker-compose.yml`.
## Остановка
- `docker compose down`

View File

@@ -1,24 +0,0 @@
version: "3.9"
services:
web:
build: .
ports:
- "8080:80"
volumes:
- ../auth:/var/www/html/auth:ro
depends_on:
- db
db:
image: mariadb:11
environment:
MARIADB_ROOT_PASSWORD: rootpass
MARIADB_DATABASE: auth_db
volumes:
- ../db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
- ./seed.sql:/docker-entrypoint-initdb.d/02-seed.sql:ro
- db_data:/var/lib/mysql
volumes:
db_data:

View File

@@ -1,5 +0,0 @@
INSERT INTO users (login, password_hash)
VALUES ('demo', '$2y$10$MyDhXOP.UZAHJcE1KWoQU.PJfxa5C82FePAuZYphCMaXya.Ylxlne');
INSERT INTO user_access (user_id, site_key)
VALUES (1, 'localhost');

74
import_users.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
// Usage: place this script next to users.txt and run: php import_users.php
// It will create users_with_passwords.txt with "login:password" lines.
require_once __DIR__ . '/config/db.php';
$inputFile = __DIR__ . '/users.txt';
$outputFile = __DIR__ . '/users_with_passwords.txt';
if (!file_exists($inputFile)) {
fwrite(STDERR, "Input file not found: {$inputFile}\n");
exit(1);
}
$lines = file($inputFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
fwrite(STDERR, "Failed to read input file.\n");
exit(1);
}
$charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$charsetLen = strlen($charset);
function generatePassword(string $charset, int $charsetLen, int $length = 6): string
{
$password = '';
for ($i = 0; $i < $length; $i++) {
$password .= $charset[random_int(0, $charsetLen - 1)];
}
return $password;
}
$pdo->beginTransaction();
try {
$selectStmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$insertStmt = $pdo->prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)");
$updateStmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
$out = fopen($outputFile, 'w');
if ($out === false) {
throw new RuntimeException('Failed to open output file for writing.');
}
$seen = [];
foreach ($lines as $line) {
$username = trim($line);
if ($username === '' || isset($seen[$username])) {
continue;
}
$seen[$username] = true;
$password = generatePassword($charset, $charsetLen, 6);
$hash = password_hash($password, PASSWORD_BCRYPT);
$selectStmt->execute([$username]);
$user = $selectStmt->fetch(PDO::FETCH_ASSOC);
if ($user) {
$updateStmt->execute([$hash, $user['id']]);
} else {
$insertStmt->execute([$username, $hash]);
}
fwrite($out, $username . ':' . $password . PHP_EOL);
}
fclose($out);
$pdo->commit();
echo "Done. Output: {$outputFile}\n";
} catch (Throwable $e) {
$pdo->rollBack();
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
exit(1);
}
?>

88
index.html Normal file
View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title>БЛА БлД СТ «Орлан-7»</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<meta name="site-alias" content="o7">
<link type="text/css" rel="stylesheet" href="/css/fonts.css">
<link type="text/css" rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="css/auth.css">
<script type="module" src="/js/window.js"></script>
</head>
<body style="display: none">
<!-- Авторизация ёпта -->
<div class="card">
<div id="user-info"></div>
<div class="actions">
<button class="btn-primary" onclick="logout()">Выйти</button>
</div>
</div>
<script>
// Проверка авторизации при загрузке страницы
async function checkAuth() {
const response = await fetch('api/check_auth.php', { credentials: 'same-origin' });
const result = await response.json();
if (!result.isLoggedIn) {
window.location.href = 'login.html';
} else {
document.getElementById('user-info').innerHTML = `
<h2>Добро пожаловать, ${result.user.username}!</h2>
`;
}
}
async function logout() {
await fetch('api/logout.php', {
method: 'POST',
credentials: 'same-origin'
});
window.location.href = 'login.html';
}
// Проверка авторизации
checkAuth();
</script>
<!-- Вот и фсе ребята -->
<div id="background">
<div class="border">
</div>
<div id="code">
<span>Комплект стендов учебных комплекса с БЛА БлД СТ АЦИЕ.01181-01</span>
</div>
<div class="header">
<img class="top-bar" src="/assets/img/tinybar.png"/>
<img class="center-bar" src="/assets/img/centerbar.png"/>
<img class="bottom-bar" src="/assets/img/tinybar.png"/>
<div>
<img class="logo" src="/assets/img/logo.svg"/>
<img class="logo-text" src="/assets/img/logo_text.svg"/>
</div>
<div class="title">
<span>
БЛА БлД СТ «Орлан-7»
</span>
</div>
</div>
</div>
<div class="select-menu">
<div class="point-menu">
<a href="/okr/polebla/orlan7/stand/stand7/orlan7.html?back=1">БЛА БлД СТ «Орлан-7»</a>
</div>
<div class="point-menu disable">
<a href="/okr/polebla/orlan7/stand/power7/power7.html?back=1">Силовая установка БЛА БлД СТ «Орлан-7»</a>
</div>
<div class="point-menu disable">
<a href="/okr/polebla/orlan7/stand/sau7/sau7.html?back=1">САУ БЛА БлД СТ «Орлан-7»</a>
</div>
<div class="point-menu disable">
<a href="/okr/polebla/orlan7/stand/landsystem7/landsystem7.html?back=1">Система посадки БЛА БлД СТ «Орлан-7»</a>
</div>
</div>
<div id="info-button">
<img src="/assets/img/info.png"></img>
</div>
</body>
</html>

68
js/auth.js Normal file
View File

@@ -0,0 +1,68 @@
// Общие функции для AJAX запросов
async function sendRequest(url, data) {
try {
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (!response.ok) {
return { success: false, message: 'Ошибка сервера' };
}
return await response.json();
} catch (error) {
console.error('Ошибка:', error);
return { success: false, message: 'Ошибка сети' };
}
}
// Обработка регистрации
if (document.getElementById('registerForm')) {
document.getElementById('registerForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
const result = await sendRequest('api/register.php', data);
if (result.success) {
alert(result.message);
window.location.href = 'login.html';
} else {
alert('Ошибка: ' + result.message);
}
});
}
// Обработка входа
if (document.getElementById('loginForm')) {
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
if (!data.site_alias) {
const metaAlias = document.querySelector('meta[name="site-alias"]');
if (metaAlias && metaAlias.content) {
data.site_alias = metaAlias.content.trim();
} else {
const path = window.location.pathname.replace(/\/+$/, '');
const parts = path.split('/').filter(Boolean);
data.site_alias = parts[0] || 'root';
}
}
const result = await sendRequest('api/login.php', data);
if (result.success) {
alert(result.message);
window.location.href = 'index.html';
} else {
alert('Ошибка: ' + result.message);
}
});
}

29
login.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="site-alias" content="o7">
<link rel="stylesheet" href="css/auth.css">
<title>Вход</title>
</head>
<body>
<div class="card">
<h2>Вход</h2>
<p class="subtitle">Для доступа к стенду авторизуйтесь</p>
<form id="loginForm">
<div class="field">
<label for="username">Логин</label>
<input id="username" type="text" name="username" placeholder="Введите логин" required autocomplete="username" autocapitalize="off" spellcheck="false">
</div>
<div class="field">
<label for="password">Пароль</label>
<input id="password" type="password" name="password" placeholder="Введите пароль" required autocomplete="current-password">
</div>
<button type="submit">Войти</button>
</form>
<div class="helper">Доступ выдается по согласованию с комиссией ПДТК и с ГК ОКР</div>
</div>
<script src="js/auth.js"></script>
</body>
</html>

6
users.txt Normal file
View File

@@ -0,0 +1,6 @@
ane.marin
r.abramov
a.agafonov
m.arkhipov
d.zaitsev
vlv.kuneevskii

6
users_with_passwords.txt Normal file
View File

@@ -0,0 +1,6 @@
ane.marin:KhfG4c
r.abramov:WAtY45
a.agafonov:XsmiAE
m.arkhipov:4nWj6J
d.zaitsev:ZgvacV
vlv.kuneevskii:RoAPfv