admin without rights

This commit is contained in:
Владимир
2026-01-09 14:08:08 +00:00
parent f5c68bf0c7
commit 36084ba590
9 changed files with 353 additions and 433 deletions

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class AuthController extends Controller
{
// Регистрация нового пользователя
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|min:6',
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Ошибка валидации',
'errors' => $validator->errors()
], 422);
}
$user = \App\Models\User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password),
'phone' => $request->phone ?? null,
'role' => 'client', // по умолчанию клиент
]);
// Создаём токен для Sanctum
$token = $user->createToken('main-token')->plainTextToken;
return response()->json([
'message' => 'Пользователь создан',
'user' => $user,
'token' => $token
], 201);
}
// Вход
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'Неверный email или пароль'
], 401);
}
$user = Auth::user();
$token = $user->createToken('main-token')->plainTextToken;
return response()->json([
'message' => 'Успешный вход',
'user' => $user,
'token' => $token
]);
}
}

View File

@@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void
{
//таблица user
Schema::create('user', function (Blueprint $table) {
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();

View File

@@ -29,6 +29,7 @@ services:
####################################################################################################
db:
image: mysql:8.1
command: --default-authentication-plugin=mysql_native_password
ports:
- 3306:3306
volumes:

View File

@@ -171,260 +171,21 @@
</div>
<script>
let token = localStorage.getItem('token');
let selectedEmployeeId = null;
let selectedDate = null;
let currentMonth = new Date().getMonth();
let currentYear = new Date().getFullYear();
let employees = [];
let intervals = [];
const token = localStorage.getItem('auth_token');
if (!token) {
window.location.href = '/register-login.html';
}
// Проверка авторизации
if (!token) {
window.location.href = 'register-login.html';
}
// Загрузка сотрудников при старте
window.onload = async function() {
await loadEmployees();
};
// Загрузить список сотрудников
async function loadEmployees() {
try {
const response = await fetch('/api/admin/users?role=employee', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const allUsers = await response.json();
employees = allUsers.filter(user => user.role === 'employee' || user.role === 'admin');
const select = document.getElementById('employeeSelect');
select.innerHTML = '<option value="">— Выберите сотрудника —</option>';
for (let employee of employees) {
select.innerHTML += `<option value="${employee.id}">${employee.name}</option>`;
}
}
} catch (error) {
alert('Ошибка загрузки сотрудников');
}
}
// При смене сотрудника
function loadEmployeeSchedule() {
const select = document.getElementById('employeeSelect');
selectedEmployeeId = select.value;
if (selectedEmployeeId) {
document.getElementById('employeeInfo').style.display = 'block';
document.getElementById('selectedEmployeeId').textContent = selectedEmployeeId;
document.getElementById('selectedEmployeeName').textContent = select.options[select.selectedIndex].text;
document.getElementById('calendarSection').style.display = 'block';
renderCalendar();
} else {
document.getElementById('calendarSection').style.display = 'none';
document.getElementById('timeIntervalsSection').style.display = 'none';
document.getElementById('employeeInfo').style.display = 'none';
}
}
// Простой календарь
function renderCalendar() {
const calendarDays = document.getElementById('calendarDays');
const monthYear = document.getElementById('monthYear');
monthYear.textContent = new Date(currentYear, currentMonth).toLocaleDateString('ru', {
year: 'numeric', month: 'long'
});
calendarDays.innerHTML = '';
const firstDay = new Date(currentYear, currentMonth, 1).getDay();
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
let dayCounter = 1 - firstDay + 1;
for (let i = 0; i < 42; i++) {
const day = document.createElement('div');
day.className = 'calendar-day';
if (dayCounter < 1) {
day.textContent = new Date(currentYear, currentMonth, 0).getDate() + dayCounter;
day.classList.add('other-month');
dayCounter++;
} else if (dayCounter <= daysInMonth) {
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(dayCounter).padStart(2, '0')}`;
day.textContent = dayCounter;
day.onclick = () => selectDate(dateStr);
dayCounter++;
} else {
day.textContent = dayCounter - daysInMonth;
day.classList.add('other-month');
dayCounter++;
}
calendarDays.appendChild(day);
}
}
function selectDate(date) {
selectedDate = date;
document.querySelectorAll('.calendar-day').forEach(day => day.classList.remove('selected'));
event.target.classList.add('selected');
document.getElementById('selectedDateTitle').textContent = `Интервалы на ${new Date(date).toLocaleDateString('ru')}`;
loadIntervals();
}
function prevMonth() {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar();
}
function nextMonth() {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar();
}
// Загрузить интервалы для даты
async function loadIntervals() {
try {
document.getElementById('timeIntervalsSection').style.display = 'block';
const response = await fetch(`/api/admin/availabilities?employee_id=${selectedEmployeeId}&date=${selectedDate}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
intervals = await response.json();
renderIntervals();
} catch (error) {
intervals = [];
renderIntervals();
}
}
// Отобразить интервалы
function renderIntervals() {
const container = document.getElementById('intervalsList');
if (intervals.length === 0) {
container.innerHTML = '<div class="no-intervals">Нет интервалов на эту дату</div>';
return;
}
container.innerHTML = '';
for (let interval of intervals) {
const item = document.createElement('div');
item.className = 'interval-item';
item.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<strong>${interval.starttime.slice(0,5)} - ${interval.endtime.slice(0,5)}</strong>
<div>
<label style="margin-right: 15px;">
<input type="checkbox" ${interval.isavailable ? '' : 'checked'} onchange="updateInterval(${interval.id}, this.checked)">
Недоступен
</label>
<button class="delete-interval" onclick="deleteInterval(${interval.id})" title="Удалить">×</button>
</div>
</div>
`;
container.appendChild(item);
}
}
// Добавить интервал
async function addInterval(event) {
event.preventDefault();
const startTime = document.getElementById('startTime').value;
const endTime = document.getElementById('endTime').value;
const isUnavailable = document.getElementById('isUnavailable').checked;
if (!startTime || !endTime) {
alert('Заполните время начала и окончания');
return;
}
try {
const response = await fetch('/api/admin/availabilities', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
employee_id: selectedEmployeeId,
date: selectedDate,
starttime: startTime + ':00',
endtime: endTime + ':00',
isavailable: !isUnavailable
})
});
if (response.ok) {
document.getElementById('startTime').value = '';
document.getElementById('endTime').value = '';
document.getElementById('isUnavailable').checked = false;
loadIntervals();
}
} catch (error) {
alert('Ошибка добавления интервала');
}
}
// Обновить доступность
async function updateInterval(id, isAvailable) {
try {
const response = await fetch(`/api/admin/availabilities/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ isavailable: isAvailable })
});
if (!response.ok) {
alert('Ошибка обновления');
loadIntervals(); // Вернуть как было
}
} catch (error) {
alert('Ошибка сети');
loadIntervals();
}
}
// Удалить интервал
async function deleteInterval(id) {
if (confirm('Удалить интервал?')) {
try {
const response = await fetch(`/api/admin/availabilities/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
loadIntervals();
}
} catch (error) {
alert('Ошибка удаления');
}
}
}
function logout() {
localStorage.removeItem('token');
window.location.href = 'index.html';
}
fetch('/api/admin/availabilities', {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(r => r.json())
.then(data => {
const list = document.getElementById('schedule-list');
data.forEach(avail => {
list.innerHTML += `<p>${avail.employee_id} — ${avail.date} ${avail.starttime} - ${avail.endtime}</p>`;
});
});
</script>
</body>
</html>

View File

@@ -180,176 +180,45 @@
</div>
<script>
let token = localStorage.getItem('token');
let services = [];
let selectedServiceId = null;
// Получаем токен из localStorage
const token = localStorage.getItem('auth_token');
// Проверка авторизации и роли
if (!token) {
window.location.href = 'register-login.html';
// Если нет токена — перенаправляем на вход
if (!token) {
alert('Доступ только для администраторов!');
window.location.href = '/register-login.html';
}
// Загружаем услуги
fetch('/api/admin/services', {
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/json'
}
// Загрузка услуг при загрузке страницы
window.onload = loadServices;
// Загрузить услуги из API
async function loadServices() {
try {
const response = await fetch('/api/admin/services', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
services = await response.json();
renderServices();
} else {
alert('Ошибка доступа или загрузки услуг');
window.location.href = 'register-login.html';
}
} catch (error) {
alert('Ошибка сети');
}
}
// Отобразить услуги в таблице
function renderServices() {
const tbody = document.getElementById('servicesTable');
if (services.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 40px; color: #666;">Нет услуг</td></tr>';
return;
}
tbody.innerHTML = '';
// Простой цикл для каждой услуги
for (let service of services) {
const row = document.createElement('tr');
const statusClass = service.isactive ? 'active' : 'inactive';
row.innerHTML = `
<td>${service.id}</td>
<td><strong>${service.name}</strong></td>
<td>${service.description || '—'}</td>
<td>${service.durationminutes} мин</td>
<td>${service.price}₽</td>
<td><span class="status-badge status-${statusClass}">${service.isactive ? 'Активна' : 'Неактивна'}</span></td>
<td>
<div class="action-buttons">
<button class="btn-small btn-edit" onclick="editService(${service.id})">
✏️ Редактировать
</button>
<button class="btn-small btn-toggle" onclick="toggleService(${service.id}, ${service.isactive})">
${service.isactive ? '❌ Деактивировать' : '✅ Активировать'}
</button>
<button class="btn-small btn-delete" onclick="showDeleteModal(${service.id}, '${service.name}')">
🗑️ Удалить
</button>
</div>
</td>
`;
tbody.appendChild(row);
}
}
// Добавить новую услугу
async function addService(event) {
event.preventDefault();
const formData = {
name: document.getElementById('serviceName').value,
description: document.getElementById('serviceDescription').value,
durationminutes: parseInt(document.getElementById('serviceDuration').value),
price: parseInt(document.getElementById('servicePrice').value),
isactive: true
};
try {
const response = await fetch('/api/admin/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(formData)
});
if (response.ok) {
alert('✅ Услуга создана!');
document.getElementById('addServiceForm').reset();
loadServices(); // Перезагрузить таблицу
} else {
const error = await response.json();
alert('Ошибка: ' + error.error || error.message);
}
} catch (error) {
alert('Ошибка сети');
}
}
// Переключить статус (активна/неактивна)
async function toggleService(id, isActive) {
try {
const response = await fetch(`/api/admin/services/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ isactive: !isActive })
});
if (response.ok) {
alert(`✅ Услуга ${!isActive ? 'активирована' : 'деактивирована'}`);
loadServices();
}
} catch (error) {
alert('Ошибка сети');
}
}
// Показать модальное окно удаления
function showDeleteModal(id, name) {
selectedServiceId = id;
document.getElementById('deleteServiceName').textContent = name;
document.getElementById('deleteModal').style.display = 'block';
}
// Удалить услугу
async function confirmDelete() {
try {
const response = await fetch(`/api/admin/services/${selectedServiceId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
alert('✅ Услуга удалена');
closeModal();
loadServices();
}
} catch (error) {
alert('Ошибка сети');
}
}
// Закрыть модальное окно
function closeModal() {
document.getElementById('deleteModal').style.display = 'none';
}
// Выход
function logout() {
localStorage.removeItem('token');
window.location.href = 'index.html';
}
// Закрытие модального окна по клику вне области
window.onclick = function(event) {
const modal = document.getElementById('deleteModal');
if (event.target === modal) closeModal();
})
.then(response => {
if (!response.ok) {
throw new Error('Ошибка сервера: ' + response.status);
}
return response.json();
})
.then(data => {
const servicesList = document.getElementById('services-list');
data.forEach(service => {
const item = document.createElement('div');
item.innerHTML = `
<h3>${service.name}</h3>
<p>Цена: ${service.price} ₽</p>
<p>Описание: ${service.description || 'Нет описания'}</p>
<hr>
`;
servicesList.appendChild(item);
});
})
.catch(error => {
console.error('Ошибка:', error);
document.body.innerHTML = `<h2 style="color: red;">Ошибка загрузки данных: ${error.message}</h2>`;
});
</script>
</body>
</html>

View File

@@ -93,7 +93,6 @@
</style>
</head>
<body>
<!-- Хедер -->
<header>
<div class="header-content">
<div class="logo">🧹 КлинСервис</div>
@@ -104,7 +103,6 @@
</div>
</header>
<!-- Hero секция -->
<section class="hero">
<div class="hero-content">
<h1>Профессиональная уборка</h1>
@@ -116,7 +114,6 @@
</div>
</section>
<!-- Услуги -->
<section class="services" id="services">
<h2 class="section-title">Наши услуги</h2>
<div class="services-grid">
@@ -141,7 +138,6 @@
</div>
</section>
<!-- Футер -->
<footer>
<div class="footer-content">
<h3>🧹 КлинСервис</h3>
@@ -156,7 +152,7 @@
</footer>
<script>
// Простая прокрутка к секции услуг
// прокрутка к секции услуг
document.querySelector('.hero-buttons .btn-secondary').onclick = function(e) {
e.preventDefault();
document.getElementById('services').scrollIntoView({

View File

@@ -1,20 +1,22 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
$request = Request::capture()
)->send();
$kernel->terminate($request, $response);

223
public/register-login.html Normal file
View File

@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход и регистрация</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
padding: 20px;
}
.container {
max-width: 500px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #5a67d8;
}
.message {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.error {
background-color: #ffebee;
color: #c62828;
}
.success {
background-color: #e8f5e9;
color: #2e7d32;
}
.toggle {
text-align: center;
margin-top: 15px;
color: #667eea;
cursor: pointer;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<!-- Форма входа -->
<div id="loginSection">
<h2>Вход</h2>
<div id="loginResult"></div>
<div class="form-group">
<label for="emailLogin">Email:</label>
<input type="email" id="emailLogin" required>
</div>
<div class="form-group">
<label for="passwordLogin">Пароль:</label>
<input type="password" id="passwordLogin" required>
</div>
<button onclick="loginUser()">Войти</button>
<div class="toggle" onclick="showRegisterForm()">Нет аккаунта? Зарегистрироваться</div>
</div>
<!-- Форма регистрации -->
<div id="registerSection" style="display: none;">
<h2>Регистрация</h2>
<div id="registerResult"></div>
<div class="form-group">
<label for="nameReg">Имя:</label>
<input type="text" id="nameReg" required>
</div>
<div class="form-group">
<label for="emailReg">Email:</label>
<input type="email" id="emailReg" required>
</div>
<div class="form-group">
<label for="passwordReg">Пароль:</label>
<input type="password" id="passwordReg" required>
</div>
<div class="form-group">
<label for="phoneReg">Телефон (не обязательно):</label>
<input type="tel" id="phoneReg">
</div>
<button onclick="registerUser()">Зарегистрироваться</button>
<div class="toggle" onclick="showLoginForm()">Уже есть аккаунт? Войти</div>
</div>
</div>
<script>
// Показать форму регистрации
function showRegisterForm() {
document.getElementById('loginSection').style.display = 'none';
document.getElementById('registerSection').style.display = 'block';
}
// Показать форму входа
function showLoginForm() {
document.getElementById('registerSection').style.display = 'none';
document.getElementById('loginSection').style.display = 'block';
}
// Функция регистрации
function registerUser() {
// Собираем данные из формы
var name = document.getElementById('nameReg').value;
var email = document.getElementById('emailReg').value;
var password = document.getElementById('passwordReg').value;
var phone = document.getElementById('phoneReg').value || null;
// Подготавливаем данные для отправки
var data = {
name: name,
email: email,
password: password,
phone: phone
};
// Отправляем POST-запрос на сервер
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/register", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var resultDiv = document.getElementById('registerResult');
if (xhr.status === 200 || xhr.status === 201) {
resultDiv.className = "message success";
resultDiv.innerHTML = "Регистрация прошла успешно! Теперь войдите.";
// Через 2 секунды переключим на форму входа
setTimeout(function() {
showLoginForm();
resultDiv.innerHTML = "";
}, 2000);
} else {
resultDiv.className = "message error";
try {
var response = JSON.parse(xhr.responseText);
resultDiv.innerHTML = response.message || "Ошибка при регистрации";
} catch (e) {
resultDiv.innerHTML = "Неизвестная ошибка сервера";
}
}
}
};
xhr.send(JSON.stringify(data));
}
// Функция входа
function loginUser() {
var email = document.getElementById('emailLogin').value;
var password = document.getElementById('passwordLogin').value;
var data = {
email: email,
password: password
};
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/login", true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var resultDiv = document.getElementById('loginResult');
if (xhr.status === 200) {
resultDiv.className = "message success";
resultDiv.innerHTML = "Вход выполнен!";
// Сохраняем токен в localStorage (чтобы потом использовать)
var response = JSON.parse(xhr.responseText);
localStorage.setItem('auth_token', response.token);
// Перенаправляем на главную
setTimeout(function() {
window.location.href = "/";
}, 1000);
} else {
resultDiv.className = "message error";
try {
var response = JSON.parse(xhr.responseText);
resultDiv.innerHTML = response.message || "Неверный email или пароль";
} catch (e) {
resultDiv.innerHTML = "Ошибка при входе";
}
}
}
};
xhr.send(JSON.stringify(data));
}
</script>
</body>
</html>

View File

@@ -7,6 +7,7 @@ use App\Http\Controllers\AvailabilitiesController;
use App\Http\Controllers\CategoriesController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
Route::get('/user', function (Request $request) {
return $request->user();
@@ -15,6 +16,8 @@ Route::get('/user', function (Request $request) {
// РЕГИСТРАЦИЯ ТОЛЬКО КЛИЕНТОВ (публичный)
Route::post('/register', [UserController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Существующие роуты categories
Route::get('/categories', [CategoriesController::class, 'index'])->middleware('auth:sanctum');
Route::get('/categories/{id}', [CategoriesController::class, 'show']);
@@ -24,7 +27,7 @@ Route::post('/categories', [CategoriesController::class, 'create']);
Route::get('/availability', [AvailabilitiesController::class, 'publicAvailability']);
// КЛИЕНТСКИЕ РОУТЫ БРОНИРОВАНИЙ (auth:sanctum)
Route::middleware('auth:sanctum')->group(function () {
Route::middleware('auth:sanctum', 'role:admin')->group(function () {
Route::post('/bookings', [BookingsController::class, 'store']);
Route::post('/bookings/{id}/cancel', [BookingsController::class, 'cancel']);
Route::post('/bookings/{id}/cancel', [BookingsController::class, 'adminCancel']);