561 lines
17 KiB
Markdown
561 lines
17 KiB
Markdown
# Логин в Fission Console — Полное описание логики
|
||
|
||
**Дата:** май 2026
|
||
**Версия консоли:** v1.3.20+
|
||
**Статус:** Рабочая конфигурация (необходимо восстановить функции логина в app.js)
|
||
|
||
---
|
||
|
||
## 1. Архитектура логина
|
||
|
||
```
|
||
Пользователь вводит токен
|
||
↓
|
||
[login.html] форма (вводится токен + стенд)
|
||
↓
|
||
[onclick="doLogin()"] → JavaScript функция
|
||
↓
|
||
POST /console/api/auth {token, env}
|
||
↓
|
||
API Cloud: проверка токена (валидность, срок действия)
|
||
↓
|
||
Успех? → localStorage сохранение → showMain()
|
||
Ошибка? → errEl.textContent → showError()
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Первоначальная загрузка (инициализация)
|
||
|
||
### 2.1 Загрузка HTML и JS
|
||
|
||
1. **index.html** загружается (основной SPA файл)
|
||
2. **css/styles.css** импортируется через `<link>` в `<head>`
|
||
3. **app.js** загружается в конце `<body>` через `<script src="js/app.js"></script>`
|
||
4. **login.html** подгружается динамически:
|
||
```javascript
|
||
// app.js, строка 433
|
||
fetch('templates/modals/login.html')
|
||
.then(r => r.text())
|
||
.then(html => {
|
||
document.body.insertAdjacentHTML('afterbegin', html);
|
||
});
|
||
```
|
||
|
||
### 2.2 Инициализация приложения
|
||
|
||
При загрузке app.js (строка 790-794):
|
||
```javascript
|
||
initializeEventListeners(); // Event delegation для data-* атрибутов
|
||
checkAuth(); // Проверка авторизации
|
||
```
|
||
|
||
**checkAuth()** функция (строка 648-658):
|
||
```javascript
|
||
function checkAuth() {
|
||
var storedToken = localStorage.getItem('auth_token');
|
||
if (!storedToken) {
|
||
showLoginOverlay(); // Показать форму логина
|
||
return;
|
||
}
|
||
setUserAvatar(storedToken); // Установить аватар с email
|
||
hideLoginOverlay(); // Скрыть форму логина
|
||
reloadAll(); // Загрузить данные
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. HTML структура логина
|
||
|
||
### 3.1 Форма входа (templates/modals/login.html)
|
||
|
||
```html
|
||
<div id="login-overlay">
|
||
<!-- Выбор стенда -->
|
||
<select id="l-env">
|
||
<option value="dev">Dev</option>
|
||
<option value="test" selected>Test</option>
|
||
<option value="prod">Prod</option>
|
||
</select>
|
||
|
||
<!-- Ввод токена -->
|
||
<textarea id="l-token" placeholder="Введите токен..."></textarea>
|
||
|
||
<!-- Вывод ошибок -->
|
||
<div id="l-error" style="display:none; color:#ff6b6b;">
|
||
<!-- Здесь показываются ошибки валидации -->
|
||
</div>
|
||
|
||
<!-- Кнопка входа -->
|
||
<button id="l-btn" onclick="doLogin()">Войти</button>
|
||
</div>
|
||
```
|
||
|
||
### 3.2 Интеграция с UI
|
||
|
||
- **Стенды:** dev/test/prod (соответствуют разным окружениям облака)
|
||
- **Токен:** JWT токен пользователя из личного кабинета
|
||
- **Email:** извлекается из JWT при успешном логине
|
||
|
||
---
|
||
|
||
## 4. Процесс логина (подробно)
|
||
|
||
### 4.1 Нажатие кнопки "Войти"
|
||
|
||
**HTML:**
|
||
```html
|
||
<button id="l-btn" onclick="doLogin()">Войти</button>
|
||
```
|
||
|
||
**Функция doLogin()** (из auth.js, строка 5):
|
||
```javascript
|
||
async function doLogin() {
|
||
// 1. Получить значения из формы
|
||
var btn = document.getElementById('l-btn');
|
||
var errEl = document.getElementById('l-error');
|
||
var token = (document.getElementById('l-token').value || '').trim();
|
||
var env = document.getElementById('l-env').value;
|
||
|
||
// 2. Базовая валидация (не пусто)
|
||
if (!token) {
|
||
errEl.textContent = 'Введите токен';
|
||
errEl.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// 3. Заблокировать кнопку, очистить ошибки
|
||
btn.disabled = true;
|
||
errEl.style.display = 'none';
|
||
|
||
try {
|
||
// 4. ОТПРАВИТЬ НА ПРОВЕРКУ К API ОБЛАКА
|
||
const res = await fetch(API_BASE + '/auth', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token: token, env: env })
|
||
});
|
||
|
||
// 5. Обработать ответ
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(() => ({}));
|
||
throw new Error(d.error || 'Ошибка входа');
|
||
}
|
||
|
||
// 6. УСПЕХ: СОХРАНИТЬ В localStorage
|
||
localStorage.setItem('auth_token', token);
|
||
localStorage.setItem('auth_env', env);
|
||
|
||
// 7. ПОКАЗАТЬ АВАТАР С EMAIL
|
||
setUserAvatar(token);
|
||
|
||
// 8. Скрыть форму логина
|
||
hideLoginOverlay();
|
||
|
||
// 9. ПРОВЕРИТЬ СТАТУС NAMESPACE
|
||
// Если namespace не готов (not ready) → показать init overlay
|
||
// и ждать пока инициализируется
|
||
try {
|
||
var sr = await fetch(API_BASE + '/ns/status',
|
||
{ headers: { 'X-Auth-Token': token, 'X-Auth-Env': env } });
|
||
var sd = await sr.json();
|
||
if (!sd.ready) {
|
||
pollNSStatus(); // Начать полинг (ждать готовности)
|
||
return;
|
||
}
|
||
} catch (_) { }
|
||
|
||
// 10. ЗАГРУЗИТЬ ОСНОВНОЙ ИНТЕРФЕЙС
|
||
reloadAll();
|
||
|
||
} catch (e) {
|
||
// Ошибка: показать сообщение
|
||
errEl.textContent = e.message;
|
||
errEl.style.display = 'block';
|
||
} finally {
|
||
// Разблокировать кнопку
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Сохранение состояния в браузер
|
||
|
||
### 5.1 localStorage: Что сохраняется
|
||
|
||
```javascript
|
||
// В момент успешного логина:
|
||
localStorage.setItem('auth_token', 'eyJhbGc...'); // JWT токен пользователя
|
||
localStorage.setItem('auth_env', 'test'); // Выбранное окружение
|
||
```
|
||
|
||
**Как долго сохраняется:**
|
||
- localStorage хранит данные до явного удаления (`localStorage.removeItem()`)
|
||
- Не удаляется при закрытии браузера (в отличие от sessionStorage)
|
||
- Восстанавливается при перезагрузке страницы
|
||
|
||
### 5.2 Восстановление сессии при перезагрузке
|
||
|
||
**Сценарий:**
|
||
1. Пользователь вошёл (токен в localStorage)
|
||
2. Закрыл браузер
|
||
3. Открыл консоль снова
|
||
|
||
**Процесс:**
|
||
1. Загружается app.js
|
||
2. Вызывается `checkAuth()` (строка 794)
|
||
3. Функция проверяет `localStorage.getItem('auth_token')`
|
||
4. Если токен есть → автоматический вход (setUserAvatar + hideLoginOverlay + reloadAll)
|
||
5. Если токена нет → показать форму логина
|
||
|
||
---
|
||
|
||
## 6. Использование токена в API запросах
|
||
|
||
### 6.1 authHeaders() функция
|
||
|
||
Каждый API запрос содержит авторизационные заголовки:
|
||
|
||
```javascript
|
||
function authHeaders() {
|
||
return {
|
||
'X-Auth-Token': localStorage.getItem('auth_token') || '',
|
||
'X-Auth-Env': localStorage.getItem('auth_env') || 'test'
|
||
};
|
||
}
|
||
```
|
||
|
||
### 6.2 Типы API запросов
|
||
|
||
**1. getJSON(url)** — GET с авторизацией
|
||
```javascript
|
||
async function getJSON(url) {
|
||
const r = await fetch(url, { headers: authHeaders() });
|
||
if (r.status === 401) { doLogout(); /* сессия истекла */ }
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**2. requestJSON(url, method, body)** — POST/PUT/DELETE с авторизацией
|
||
```javascript
|
||
async function requestJSON(url, method, body) {
|
||
const r = await fetch(url, {
|
||
method: method,
|
||
headers: Object.assign(
|
||
{ 'Content-Type': 'application/json' },
|
||
authHeaders()
|
||
),
|
||
body: body ? JSON.stringify(body) : undefined
|
||
});
|
||
if (r.status === 401) { doLogout(); /* сессия истекла */ }
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Проверка токена на API облака
|
||
|
||
### 7.1 Эндпоинт /console/api/auth
|
||
|
||
**Запрос:**
|
||
```http
|
||
POST /console/api/auth
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"env": "test"
|
||
}
|
||
```
|
||
|
||
**Ответ (успех — 200):**
|
||
```json
|
||
{
|
||
"success": true
|
||
}
|
||
```
|
||
|
||
**Ответ (ошибка — 400/401):**
|
||
```json
|
||
{
|
||
"error": "Invalid token" | "Token expired" | "Unknown environment"
|
||
}
|
||
```
|
||
|
||
### 7.2 Что проверяет API облака
|
||
|
||
1. **Валидность JWT токена** — подпись, структура
|
||
2. **Срок действия токена** — не истёк ли
|
||
3. **Существование пользователя** — есть ли в базе
|
||
4. **Окружение** — существует ли такой env (dev/test/prod)
|
||
5. **Права доступа** — может ли этот пользователь войти в это окружение
|
||
|
||
---
|
||
|
||
## 8. Извлечение email из JWT
|
||
|
||
### 8.1 jwtEmail() функция
|
||
|
||
```javascript
|
||
function jwtEmail(token) {
|
||
try {
|
||
// JWT формат: header.payload.signature
|
||
// payload — base64url кодирован
|
||
var payload = JSON.parse(
|
||
atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))
|
||
);
|
||
// Попытаться вытащить email или sub (subject = идентификатор)
|
||
return payload.email || payload.sub || null;
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.2 setUserAvatar() функция
|
||
|
||
```javascript
|
||
function setUserAvatar(token) {
|
||
var el = document.getElementById('user-avatar');
|
||
if (!el) return;
|
||
|
||
var email = token ? jwtEmail(token) : null;
|
||
|
||
if (email) {
|
||
// Показать email в аватаре (правый верхний угол)
|
||
el.textContent = email;
|
||
el.title = email;
|
||
el.style.fontSize = '0.7rem';
|
||
el.style.minWidth = '60px';
|
||
el.style.padding = '0 8px';
|
||
} else {
|
||
// Вернуть в дефолтное состояние (буква "N")
|
||
el.textContent = 'N';
|
||
el.title = '';
|
||
el.style.fontSize = '';
|
||
el.style.minWidth = '';
|
||
el.style.padding = '';
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Логаут
|
||
|
||
### 9.1 doLogout() функция
|
||
|
||
```javascript
|
||
function doLogout() {
|
||
// 1. Удалить токен и окружение из localStorage
|
||
localStorage.removeItem('auth_token');
|
||
localStorage.removeItem('auth_env');
|
||
|
||
// 2. Очистить форму логина
|
||
try { document.getElementById('l-token').value = ''; } catch (_) { }
|
||
|
||
// 3. Вернуть аватар в дефолт
|
||
setUserAvatar(null);
|
||
|
||
// 4. Показать форму логина
|
||
showLoginOverlay();
|
||
}
|
||
```
|
||
|
||
### 9.2 Автоматический логаут
|
||
|
||
Если API вернёт **401 (Unauthorized)**:
|
||
|
||
```javascript
|
||
// В getJSON()
|
||
if (r.status === 401) { doLogout(); throw new Error('Сессия истекла'); }
|
||
|
||
// В requestJSON()
|
||
if (r.status === 401) { doLogout(); throw new Error('Сессия истекла'); }
|
||
```
|
||
|
||
**Причины 401:**
|
||
- Токен истёк
|
||
- Токен был отозван (revoked) в облаке
|
||
- Пользователь был заблокирован
|
||
|
||
---
|
||
|
||
## 10. Показ/скрытие интерфейса
|
||
|
||
### 10.1 showLoginOverlay()
|
||
|
||
```javascript
|
||
function showLoginOverlay() {
|
||
document.getElementById('login-overlay').style.display = 'flex';
|
||
}
|
||
```
|
||
|
||
### 10.2 hideLoginOverlay()
|
||
|
||
```javascript
|
||
function hideLoginOverlay() {
|
||
document.getElementById('login-overlay').style.display = 'none';
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 11. Проверка статуса namespace
|
||
|
||
### 11.1 pollNSStatus() функция (строка 600)
|
||
|
||
После логина API проверяет готовность namespace пользователя:
|
||
|
||
```javascript
|
||
try {
|
||
var sr = await fetch(API_BASE + '/ns/status',
|
||
{ headers: { 'X-Auth-Token': token, 'X-Auth-Env': env } });
|
||
var sd = await sr.json();
|
||
if (!sd.ready) {
|
||
pollNSStatus(); // ← Начать полинг
|
||
return;
|
||
}
|
||
} catch (_) { }
|
||
```
|
||
|
||
**pollNSStatus()** (строка 600):
|
||
```javascript
|
||
function pollNSStatus() {
|
||
// Показать инит-оверлей (ждём создания namespace)
|
||
document.getElementById('init-overlay').style.display = 'flex';
|
||
|
||
// Полинг каждые 5 сек
|
||
const pollInterval = setInterval(async function() {
|
||
try {
|
||
var statusResp = await fetch(API_BASE + '/ns/status',
|
||
{ headers: authHeaders() });
|
||
var debugResp = await fetch(API_BASE + '/ns/debug',
|
||
{ headers: authHeaders() });
|
||
var statusData = await statusResp.json();
|
||
var debugData = await debugResp.json();
|
||
|
||
// Обновить UI с информацией о статусе
|
||
// Когда ready === true → загрузить основной UI
|
||
if (statusData.ready) {
|
||
clearInterval(pollInterval);
|
||
document.getElementById('init-overlay').style.display = 'none';
|
||
reloadAll(); // Загрузить данные
|
||
}
|
||
} catch (_) { }
|
||
}, 5000);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Схема данных в браузере
|
||
|
||
```
|
||
localStorage
|
||
├── auth_token: "eyJhbGc..." ← JWT токен пользователя
|
||
├── auth_env: "test" ← Выбранное окружение
|
||
└── (другие данные могут быть)
|
||
|
||
DOM элементы:
|
||
├── #login-overlay ← Форма логина
|
||
│ ├── #l-env ← Dropdown стендов
|
||
│ ├── #l-token ← Textarea с токеном
|
||
│ ├── #l-error ← Div для ошибок
|
||
│ └── #l-btn ← Кнопка "Войти"
|
||
├── #init-overlay ← Ожидание инициализации NS
|
||
└── #user-avatar ← Отображение email в navbar
|
||
```
|
||
|
||
---
|
||
|
||
## 13. Полный flow диаграмма
|
||
|
||
```
|
||
START
|
||
↓
|
||
[Загрузка index.html]
|
||
↓
|
||
[Загрузка app.js]
|
||
↓
|
||
[Загрузка login.html через fetch]
|
||
↓
|
||
[initializeEventListeners() — event delegation]
|
||
↓
|
||
[checkAuth()] — Есть ли токен в localStorage?
|
||
├─ НЕТ → [showLoginOverlay()]
|
||
│ ↓
|
||
│ [Пользователь вводит токен + env]
|
||
│ ↓
|
||
│ [onclick="doLogin()"]
|
||
│ ↓
|
||
│ [POST /console/api/auth {token, env}]
|
||
│ ↓
|
||
│ [API облака проверяет токен]
|
||
│ ├─ ОШИБКА → [errEl.display=block; errEl.textContent=...]
|
||
│ │ ↓
|
||
│ │ [Пользователь вводит заново]
|
||
│ │
|
||
│ └─ OK → [localStorage.setItem(auth_token, token)]
|
||
│ [localStorage.setItem(auth_env, env)]
|
||
│ ↓
|
||
│ [setUserAvatar(token)] — извлечь email из JWT
|
||
│ ↓
|
||
│ [hideLoginOverlay()]
|
||
│ ↓
|
||
│ [Проверить /ns/status]
|
||
│ ├─ NOT READY → [pollNSStatus()] — ждать 5сек * N
|
||
│ └─ READY → [reloadAll()]
|
||
│
|
||
└─ ДА → [setUserAvatar(token)]
|
||
[hideLoginOverlay()]
|
||
[reloadAll()] — загрузить основной UI
|
||
|
||
[Пользователь работает с консолью]
|
||
↓
|
||
[Все запросы содержат authHeaders(): X-Auth-Token + X-Auth-Env]
|
||
↓
|
||
[Если API вернёт 401]
|
||
├─ → [doLogout()] — удалить токены, вернуть на логин
|
||
|
||
[При нажатии "Выход"]
|
||
├─ → [doLogout()]
|
||
|
||
END
|
||
```
|
||
|
||
---
|
||
|
||
## 14. ТЕКУЩЕЕ СОСТОЯНИЕ (v1.3.20)
|
||
|
||
### Проблема
|
||
Функции логина **отсутствуют в app.js**:
|
||
- ❌ `doLogin()` — НЕ НАЙДЕНА
|
||
- ❌ `showLoginOverlay()` — НЕ НАЙДЕНА
|
||
- ❌ `hideLoginOverlay()` — НЕ НАЙДЕНА
|
||
- ❌ `setUserAvatar()` — НЕ НАЙДЕНА
|
||
- ❌ `jwtEmail()` — НЕ НАЙДЕНА
|
||
- ❌ `doLogout()` — НЕ НАЙДЕНА
|
||
- ❌ `pollNSStatus()` — НЕ НАЙДЕНА (строка 600 пустая)
|
||
|
||
### Почему логин всё ещё работает?
|
||
- Кнопка "Войти" имеет `onclick="doLogin()"` (inline обработчик, не через event delegation)
|
||
- Функции должны быть где-то определены, но их нет в app.js
|
||
- Возможно: отсутствуют в текущей версии после рефакторинга
|
||
|
||
### Необходимо восстановить
|
||
Все 6 функций должны быть в app.js перед инициализацией (до `initializeEventListeners()` и `checkAuth()`).
|
||
|
||
---
|
||
|
||
## 15. Ссылки на код
|
||
|
||
- [index.html](../../console/ui/index.html) — основной HTML
|
||
- [app.js](../../console/ui/js/app.js) — основная логика
|
||
- [login.html template](../../console/ui/templates/modals/login.html) — форма логина
|
||
- [auth.js (коммит f8c4b3e)](../../.git/refs/heads/main) — оригинальные функции логина
|