- Клиентская часть Vue 3 + Vite - Серверная часть Node.js + WebSocket - Система авторизации и смен - Управление игровыми портами - Поддержка тем (светлая/темная) - Адаптивный дизайн 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
467 lines
14 KiB
Markdown
467 lines
14 KiB
Markdown
# Документация по системе авторизации и выхода пользователя
|
||
|
||
## Обзор системы авторизации
|
||
|
||
Система использует двухуровневую авторизацию:
|
||
1. **Локальная авторизация** - через WebSocket с пультом
|
||
2. **Серверная авторизация** - через внешний сервер ws.tirpriz.ru
|
||
|
||
---
|
||
|
||
## Процесс авторизации пользователя
|
||
|
||
### 1. Клиентская часть (Frontend)
|
||
|
||
#### Инициализация авторизации:
|
||
```javascript
|
||
// www/start.jsc
|
||
case "avt":
|
||
// Если авторизация успешна
|
||
if (postData.user != false) {
|
||
avt = postData.user;
|
||
sessionStorage.avt = JSON.stringify(avt);
|
||
showPage(page.run, true);
|
||
alert.add && alert.add({content: "Добро пожаловать "+(avt.fio ? avt.fio : avt.tel)+"!"});
|
||
} else {
|
||
showPage(page.avt, false);
|
||
alert.add && alert.add({content: "Неверный логин или пароль!"});
|
||
}
|
||
```
|
||
|
||
### 2. Серверная часть (Backend)
|
||
|
||
#### Обработка запроса авторизации:
|
||
```javascript
|
||
// ws.js:380-410
|
||
case "avt":
|
||
let user = false;
|
||
|
||
// Авторизация по паролю
|
||
if (postData.pass && postData.tel in avt && avt[postData.tel].pass == postData.pass) {
|
||
user = JSON.parse(JSON.stringify(avt[postData.tel]));
|
||
delete user.pass;
|
||
hash[postData.tel] = genHash(15);
|
||
user.hash = hash[postData.tel];
|
||
await fs.writeFile('./data/hash.ini', JSON.stringify(hash));
|
||
}
|
||
|
||
// Авторизация по токену (hash)
|
||
if (postData.hash && postData.tel in hash && postData.tel in avt && hash[postData.tel] == postData.hash) {
|
||
user = JSON.parse(JSON.stringify(avt[postData.tel]));
|
||
user.hash = hash[postData.tel];
|
||
delete user.pass;
|
||
}
|
||
|
||
wsClient.cfg.avt = user;
|
||
|
||
if (user != false) {
|
||
// Отправляем данные пользователя
|
||
wsClient.send(JSON.stringify({
|
||
do: "avt",
|
||
user,
|
||
synch_log: log.synch_log(),
|
||
VER,
|
||
games: game.getgame(),
|
||
info: game.getInfo(),
|
||
admins: avt,
|
||
pult: game.cfg.pult,
|
||
tir: game.cfg.tir,
|
||
esp_ping: game.cfg.pingStatus
|
||
}));
|
||
|
||
// Регистрируем вход
|
||
if (!(wsClient.cfg.avt._id in game.cfg.avt)) {
|
||
log.save({do: "login", adminId: wsClient.cfg.avt._id});
|
||
game.cfg.avt[wsClient.cfg.avt._id] = true;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Процесс выхода из системы
|
||
|
||
### 1. Клиентская часть
|
||
|
||
#### Инициация выхода:
|
||
```javascript
|
||
// www/layout/menu/menu.jsc
|
||
<div class="logout" onclick="socket.send(JSON.stringify({do:'logout'}));"></div>
|
||
|
||
// www/page/run/run.jsc
|
||
<div onmousedown="socket.send(JSON.stringify({do:'logout'})); DIVconfirm.blur();">
|
||
<txt "Да">
|
||
</div>
|
||
```
|
||
|
||
#### Обработка ответа выхода:
|
||
```javascript
|
||
// www/start.jsc:166-170
|
||
case "logout":
|
||
avt = {};
|
||
delete sessionStorage.avt;
|
||
location.reload();
|
||
```
|
||
|
||
### 2. Серверная часть
|
||
|
||
#### Обработка запроса выхода:
|
||
```javascript
|
||
// ws.js:412-417
|
||
case "logout":
|
||
wsClient.send(JSON.stringify({do: "logout"}));
|
||
log.save({do: "logout", adminId: wsClient.cfg.avt._id});
|
||
wsClient.cfg.avt = false;
|
||
delete game.cfg.avt[wsClient.cfg.avt._id];
|
||
```
|
||
|
||
---
|
||
|
||
## Взаимодействие с внешним сервером
|
||
|
||
### 1. Подключение к серверу
|
||
```javascript
|
||
// ws-toserver.js:11
|
||
global.socket_to_server = new WebSocket("ws://ws.tirpriz.ru/ws");
|
||
```
|
||
|
||
### 2. Авторизация на сервере
|
||
```javascript
|
||
// ws-toserver.js:23-49
|
||
socket_to_server.onopen = async (e) => {
|
||
// Собираем информацию об администраторах
|
||
let admins = [];
|
||
wsServer.clients.forEach(client => {
|
||
if (client?.cfg?.avt) admins.push({
|
||
adminId: client.cfg.avt.id,
|
||
ip: client._socket.remoteAddress
|
||
});
|
||
});
|
||
|
||
// Формируем данные авторизации
|
||
const authData = {
|
||
do: "avt",
|
||
tip: "pult",
|
||
serialcpu,
|
||
admins,
|
||
VER,
|
||
network: os.networkInterfaces(),
|
||
disk,
|
||
freemem: os.freemem(),
|
||
totalmem: os.totalmem()
|
||
};
|
||
|
||
// Отправляем данные авторизации
|
||
socket_to_server.send(JSON.stringify(authData));
|
||
};
|
||
```
|
||
|
||
### 3. Синхронизация данных
|
||
```javascript
|
||
// ws-toserver.js:85-120
|
||
case "avt":
|
||
conn_to_server = true;
|
||
|
||
// Проверяем версии данных и обновляем если нужно
|
||
if (postData.tir && postData.tir.access.ver != ver.admin) {
|
||
let avt = await send_to_server({do: "admin/pult"});
|
||
if (avt.do = "admin/pult") {
|
||
let avt_file = {};
|
||
for (let i = 0; i < avt.admins.length; i++) {
|
||
avt_file[avt.admins[i].tel] = avt.admins[i];
|
||
}
|
||
await fs.writeFile('./data/avt.ini', JSON.stringify(avt_file));
|
||
ver.admin = avt.ver;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Система токенов (hash)
|
||
|
||
### 1. Генерация токена
|
||
```javascript
|
||
// При успешной авторизации по паролю генерируется токен
|
||
hash[postData.tel] = genHash(15);
|
||
user.hash = hash[postData.tel];
|
||
await fs.writeFile('./data/hash.ini', JSON.stringify(hash));
|
||
```
|
||
|
||
### 2. Валидация токена
|
||
```javascript
|
||
// Проверка токена при последующих запросах
|
||
if (postData.hash && postData.tel in hash && postData.tel in avt && hash[postData.tel] == postData.hash) {
|
||
user = JSON.parse(JSON.stringify(avt[postData.tel]));
|
||
user.hash = hash[postData.tel];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Роли и доступы
|
||
|
||
### 1. Проверка авторизации
|
||
```javascript
|
||
// ws.js:41
|
||
if (!wsClient.cfg.avt && postData.do != 'avt') {
|
||
// Блокируем доступ неавторизованным пользователям
|
||
return;
|
||
}
|
||
```
|
||
|
||
### 2. Типы ролей
|
||
- **Техник** - доступ к технической смене
|
||
- **Оператор** - доступ к обычной смене
|
||
- **Админ** - полный доступ
|
||
|
||
---
|
||
|
||
## Миграция на React
|
||
|
||
### 1. Рекомендуемая архитектура
|
||
|
||
#### Redux Store для авторизации:
|
||
```javascript
|
||
// store/authSlice.js
|
||
const authSlice = createSlice({
|
||
name: 'auth',
|
||
initialState: {
|
||
user: null,
|
||
isAuthenticated: false,
|
||
token: null,
|
||
loading: false,
|
||
error: null
|
||
},
|
||
reducers: {
|
||
loginStart: (state) => {
|
||
state.loading = true;
|
||
state.error = null;
|
||
},
|
||
loginSuccess: (state, action) => {
|
||
state.user = action.payload.user;
|
||
state.token = action.payload.hash;
|
||
state.isAuthenticated = true;
|
||
state.loading = false;
|
||
|
||
// Сохраняем в sessionStorage
|
||
sessionStorage.setItem('avt', JSON.stringify(action.payload.user));
|
||
},
|
||
loginFailure: (state, action) => {
|
||
state.error = action.payload;
|
||
state.loading = false;
|
||
},
|
||
logout: (state) => {
|
||
state.user = null;
|
||
state.token = null;
|
||
state.isAuthenticated = false;
|
||
sessionStorage.removeItem('avt');
|
||
},
|
||
restoreAuth: (state, action) => {
|
||
const savedAuth = sessionStorage.getItem('avt');
|
||
if (savedAuth) {
|
||
state.user = JSON.parse(savedAuth);
|
||
state.isAuthenticated = true;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
#### WebSocket хук для авторизации:
|
||
```javascript
|
||
// hooks/useAuth.js
|
||
export const useAuth = () => {
|
||
const dispatch = useDispatch();
|
||
const { user, isAuthenticated, loading, error } = useSelector(state => state.auth);
|
||
|
||
const login = async (tel, password) => {
|
||
dispatch(loginStart());
|
||
try {
|
||
const response = await sendMessage({
|
||
do: "avt",
|
||
tel,
|
||
pass: password
|
||
});
|
||
|
||
if (response.user !== false) {
|
||
dispatch(loginSuccess(response));
|
||
return { success: true };
|
||
} else {
|
||
dispatch(loginFailure("Неверный логин или пароль"));
|
||
return { success: false, error: "Неверный логин или пароль" };
|
||
}
|
||
} catch (error) {
|
||
dispatch(loginFailure(error.message));
|
||
return { success: false, error: error.message };
|
||
}
|
||
};
|
||
|
||
const logout = () => {
|
||
sendMessage({ do: "logout" });
|
||
dispatch(logout());
|
||
};
|
||
|
||
const restoreAuth = () => {
|
||
dispatch(restoreAuth());
|
||
};
|
||
|
||
return { user, isAuthenticated, loading, error, login, logout, restoreAuth };
|
||
};
|
||
```
|
||
|
||
#### Компонент авторизации:
|
||
```javascript
|
||
// components/Auth/LoginForm.jsx
|
||
const LoginForm = () => {
|
||
const [tel, setTel] = useState('');
|
||
const [password, setPassword] = useState('');
|
||
const { login, loading, error } = useAuth();
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
const result = await login(tel, password);
|
||
if (result.success) {
|
||
// Переход на главную страницу
|
||
navigate('/dashboard');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit}>
|
||
<input
|
||
type="tel"
|
||
value={tel}
|
||
onChange={(e) => setTel(e.target.value)}
|
||
placeholder="Телефон"
|
||
required
|
||
/>
|
||
<input
|
||
type="password"
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
placeholder="Пароль"
|
||
required
|
||
/>
|
||
<button type="submit" disabled={loading}>
|
||
{loading ? 'Вход...' : 'Войти'}
|
||
</button>
|
||
{error && <div className="error">{error}</div>}
|
||
</form>
|
||
);
|
||
};
|
||
```
|
||
|
||
#### Защищенные маршруты:
|
||
```javascript
|
||
// components/ProtectedRoute.jsx
|
||
const ProtectedRoute = ({ children, requiredRole }) => {
|
||
const { isAuthenticated, user } = useAuth();
|
||
|
||
if (!isAuthenticated) {
|
||
return <Navigate to="/login" replace />;
|
||
}
|
||
|
||
if (requiredRole && !hasAccess(user, requiredRole)) {
|
||
return <div>Недостаточно прав доступа</div>;
|
||
}
|
||
|
||
return children;
|
||
};
|
||
|
||
const hasAccess = (user, requiredRole) => {
|
||
switch (requiredRole) {
|
||
case 'tech':
|
||
return user.groupz?.access?.tools === true;
|
||
case 'operator':
|
||
return user.groupz?.access?.smena === true;
|
||
case 'admin':
|
||
return user.groupz?.access?.admin === true;
|
||
default:
|
||
return true;
|
||
}
|
||
};
|
||
```
|
||
|
||
### 2. WebSocket интеграция
|
||
|
||
#### WebSocket Context:
|
||
```javascript
|
||
// context/WebSocketContext.js
|
||
const WebSocketContext = createContext();
|
||
|
||
export const WebSocketProvider = ({ children }) => {
|
||
const [socket, setSocket] = useState(null);
|
||
const [connected, setConnected] = useState(false);
|
||
const dispatch = useDispatch();
|
||
|
||
useEffect(() => {
|
||
const ws = new WebSocket('ws://localhost:9000');
|
||
|
||
ws.onopen = () => {
|
||
setConnected(true);
|
||
setSocket(ws);
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
const data = JSON.parse(event.data);
|
||
|
||
switch (data.do) {
|
||
case 'avt':
|
||
if (data.user !== false) {
|
||
dispatch(loginSuccess(data));
|
||
} else {
|
||
dispatch(loginFailure("Авторизация не удалась"));
|
||
}
|
||
break;
|
||
case 'logout':
|
||
dispatch(logout());
|
||
break;
|
||
// ... другие случаи
|
||
}
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
setConnected(false);
|
||
setSocket(null);
|
||
setTimeout(() => {
|
||
// Переподключение
|
||
}, 5000);
|
||
};
|
||
|
||
return () => ws.close();
|
||
}, []);
|
||
|
||
const sendMessage = (message) => {
|
||
if (socket && connected) {
|
||
socket.send(JSON.stringify(message));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<WebSocketContext.Provider value={{ socket, connected, sendMessage }}>
|
||
{children}
|
||
</WebSocketContext.Provider>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Ключевые особенности системы
|
||
|
||
1. **Двойная авторизация** - локальная и серверная
|
||
2. **Токенная система** - hash токены сохраняются в файл
|
||
3. **Роли доступа** - техник, оператор, админ
|
||
4. **Автоматическая синхронизация** - данные пользователей обновляются с сервера
|
||
5. **Логирование** - все входы/выходы записываются в лог
|
||
6. **Восстановление сессии** - через sessionStorage
|
||
7. **Реконнект** - автоматическое переподключение к серверу
|
||
|
||
## Важные файлы для миграции
|
||
|
||
- `ws.js` - основная логика авторизации
|
||
- `ws-toserver.js` - взаимодействие с внешним сервером
|
||
- `www/start.jsc` - клиентская обработка авторизации
|
||
- `data_start/avt.ini` - пользователи системы
|
||
- `data_start/hash.ini` - токены сессий |