ваше сообщение коммита

This commit is contained in:
2025-07-09 01:18:58 +03:00
parent c18b674364
commit 81dced1f11
54 changed files with 15732 additions and 214 deletions

80
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,80 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Оптимизация производительности
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Сжатие
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Кеширование статических файлов
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary Accept-Encoding;
}
# Основные файлы HTML
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# API прокси (если нужно)
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket прокси
location /ws {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Безопасность
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
}
}

View File

@@ -1,18 +1,8 @@
import axios from 'axios';
// Определяем baseURL в зависимости от окружения
const getBaseUrl = () => {
// В браузере используем localhost
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
return 'http://localhost:8000';
}
// В других случаях используем переменную окружения
return import.meta.env.VITE_API_URL || '';
};
// Создаем экземпляр axios с базовым URL
const api = axios.create({
baseURL: getBaseUrl(),
baseURL: '/api',
withCredentials: true,
headers: {
'Content-Type': 'application/json',
@@ -29,17 +19,27 @@ api.interceptors.request.use(
(error) => Promise.reject(error)
);
// Добавляем перехватчик для обработки ответов
api.interceptors.response.use(
// Добавляем перехватчик ответов для обработки ошибок
axios.interceptors.response.use(
(response) => {
console.log('Response from server:', response.data);
// Проверяем, что ответ действительно JSON
if (response.headers['content-type'] &&
!response.headers['content-type'].includes('application/json')) {
console.warn('Server returned non-JSON response:', response.headers['content-type']);
// Если это HTML, значит, запрос ушёл не туда
if (response.headers['content-type'].includes('text/html')) {
throw new Error('Server returned HTML instead of JSON. Check API endpoint.');
}
}
return response;
},
async (error) => {
// Проверяем, что это действительно ошибка авторизации
if (error.response?.status === 401) {
// Перенаправляем на страницу логина
window.location.href = '/login';
(error) => {
// Если ошибка содержит HTML в response
if (error.response && error.response.data &&
typeof error.response.data === 'string' &&
error.response.data.includes('<!DOCTYPE')) {
console.error('API Error: Server returned HTML instead of JSON');
error.message = 'Ошибка: сервер вернул HTML вместо JSON. Проверьте подключение к API.';
}
return Promise.reject(error);
}
@@ -48,7 +48,7 @@ api.interceptors.response.use(
// Пример функции для отправки гостевого сообщения на сервер
const sendGuestMessageToServer = async (messageText) => {
try {
await axios.post('/api/chat/guest-message', {
await axios.post('/chat/guest-message', {
message: messageText,
// language: userLanguage.value, // TODO: Реализовать получение языка пользователя
});

View File

@@ -55,4 +55,14 @@ a {
a:hover {
text-decoration: underline; /* Подчеркивание при наведении */
}
html, body {
width: 100vw !important;
max-width: 100vw !important;
min-width: 0 !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
}

View File

@@ -145,7 +145,7 @@ const handleWalletAuth = async () => {
const disconnectWallet = async () => {
console.log('[BaseLayout] Выполняется выход из системы...');
try {
await api.post('/api/auth/logout');
await api.post('/auth/logout');
showSuccessMessage('Вы успешно вышли из системы');
removeFromStorage('guestMessages');
removeFromStorage('hasUserSentMessage');

View File

@@ -488,6 +488,33 @@ async function handleAiReply() {
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
max-height: 100vh;
min-height: 0;
position: relative;
}
.chat-messages {
flex: 1 1 auto;
overflow-y: auto;
position: relative;
padding-bottom: 8px;
}
.chat-input {
position: relative;
width: 100%;
margin-bottom: 12px;
margin-top: 8px;
left: 0;
right: 0;
border-radius: 12px 12px 0 0;
box-shadow: 0 -2px 8px rgba(0,0,0,0.04);
}
.chat-container {
flex: 1;
display: flex;
@@ -714,33 +741,61 @@ async function handleAiReply() {
}
@media (max-width: 480px) {
.chat-container {
margin: var(--spacing-xs) auto;
.chat-input {
position: fixed !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
z-index: 1000 !important;
margin: 0 !important;
border-radius: 12px 12px 0 0 !important;
}
.chat-messages {
padding: var(--spacing-sm);
padding-bottom: 70px !important; /* чтобы сообщения не перекрывались input */
max-height: calc(100vh - 70px) !important;
overflow-y: auto !important;
}
.input-area {
gap: 4px;
}
@media (max-width: 600px) {
.chat-input {
position: fixed !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
z-index: 1000 !important;
margin: 0 !important;
border-radius: 12px 12px 0 0 !important;
padding: 8px 12px !important;
background: #f8f8f8 !important;
border-top: 1px solid #eee !important;
box-sizing: border-box !important;
}
.chat-icon-btn {
width: 28px;
height: 28px;
.chat-messages {
padding-bottom: 70px !important; /* чтобы сообщения не перекрывались input */
max-height: calc(100vh - 70px) !important;
overflow-y: auto !important;
box-sizing: border-box !important;
}
.chat-icon-btn svg {
width: 18px;
height: 18px;
}
.preview-item {
font-size: var(--font-size-xs);
.chat-container {
padding: 0 !important;
margin: 0 !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 0 !important;
box-sizing: border-box !important;
}
}
.input-area textarea {
flex: 1 1 0%;
min-width: 0;
width: 100%;
box-sizing: border-box;
}
.ai-spinner {
animation: ai-spin 1s linear infinite;
}

View File

@@ -182,11 +182,25 @@ onBeforeUnmount(() => {
}
@media (max-width: 480px) {
.header-text {
display: none; /* Hide text on very small screens */
.title {
font-size: 1em;
text-align: left;
word-break: break-word;
}
.subtitle {
font-size: 0.7em;
text-align: left;
word-break: break-word;
}
.header-content {
justify-content: flex-end; /* Align button to the right */
flex-direction: row;
align-items: center;
}
.header-text {
flex: 1;
min-width: 0;
text-align: left;
width: auto;
}
}

View File

@@ -127,7 +127,7 @@ async function submitImport() {
return obj;
});
try {
const resp = await fetch('/api/users/import', {
const resp = await fetch('/users/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contacts)

View File

@@ -58,9 +58,9 @@ async function save() {
return;
}
if (props.rule && props.rule.id) {
await axios.put(`/api/settings/ai-assistant-rules/${props.rule.id}`, { name: name.value, description: description.value, rules });
await axios.put(`/settings/ai-assistant-rules/${props.rule.id}`, { name: name.value, description: description.value, rules });
} else {
await axios.post('/api/settings/ai-assistant-rules', { name: name.value, description: description.value, rules });
await axios.post('/settings/ai-assistant-rules', { name: name.value, description: description.value, rules });
}
emit('close', true);
}

View File

@@ -118,7 +118,7 @@ const getRagResultClass = () => {
const refreshStatus = async () => {
loading.value = true;
try {
const response = await axios.get('/api/monitoring');
const response = await axios.get('/monitoring');
monitoringData.value = response.data;
lastUpdate.value = new Date().toLocaleString('ru-RU');
} catch (e) {
@@ -133,7 +133,7 @@ const testRAG = async () => {
ragTesting.value = true;
ragResult.value = null;
try {
const response = await axios.post('/api/rag/answer', {
const response = await axios.post('/rag/answer', {
tableId: 28,
question: ragQuestion.value,
userTags: [],

View File

@@ -57,7 +57,7 @@ const emit = defineEmits(['close', 'success']);
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/request', {
const response = await axios.post('/auth/email/request', {
email: email.value,
});
if (response.data.success) {
@@ -76,7 +76,7 @@ const emit = defineEmits(['close', 'success']);
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/verify-code', {
const response = await axios.post('/auth/email/verify-code', {
email: email.value,
code: code.value,
});

View File

@@ -18,7 +18,7 @@ const updateIdentities = async () => {
if (!isAuthenticated.value || !userId.value) return;
try {
const response = await axios.get('/api/auth/identities');
const response = await axios.get('/auth/identities');
if (response.data.success) {
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities
@@ -71,7 +71,7 @@ const stopIdentitiesPolling = () => {
const checkTokenBalances = async (address) => {
try {
const response = await axios.get(`/api/auth/check-tokens/${address}`);
const response = await axios.get(`/auth/check-tokens/${address}`);
if (response.data.success) {
tokenBalances.value = response.data.balances;
return response.data.balances;
@@ -194,7 +194,7 @@ const linkMessages = async () => {
/* Удаляем ненужный вызов
try {
// Отправляем запрос на связывание сообщений
const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
const response = await axios.post('/auth/link-guest-messages', identifiersData);
if (response.data.success) {
console.log('Messages linked successfully:', response.data);
@@ -257,7 +257,7 @@ const linkMessages = async () => {
const checkAuth = async () => {
try {
const response = await axios.get('/api/auth/check');
const response = await axios.get('/auth/check');
console.log('Auth check response:', response.data);
const wasAuthenticated = isAuthenticated.value;
@@ -315,7 +315,7 @@ const checkAuth = async () => {
const disconnect = async () => {
try {
// Удаляем все идентификаторы перед выходом
await axios.post('/api/auth/logout');
await axios.post('/auth/logout');
// Обновляем состояние в памяти
updateAuth({
@@ -423,7 +423,7 @@ const updateConnectionDisplay = (isConnected, authType, authData = {}) => {
* @returns {Promise<Object>} - Результат операции
*/
const linkIdentity = async (type, value) => {
const response = await axios.post('/api/link', {
const response = await axios.post('/link', {
type,
value,
});
@@ -437,7 +437,7 @@ const linkIdentity = async (type, value) => {
* @returns {Promise<Object>} - Результат операции
*/
const deleteIdentity = async (provider, providerId) => {
const response = await axios.delete(`/api/${provider}/${encodeURIComponent(providerId)}`);
const response = await axios.delete(`/${provider}/${encodeURIComponent(providerId)}`);
return response.data;
};

View File

@@ -47,7 +47,7 @@ export function useAuthFlow(options = {}) {
telegramAuth.value.isLoading = true;
telegramAuth.value.error = '';
try {
const response = await api.post('/api/auth/telegram/init');
const response = await api.post('/auth/telegram/init');
if (response.data.success) {
telegramAuth.value.verificationCode = response.data.verificationCode;
telegramAuth.value.botLink = response.data.botLink;
@@ -130,7 +130,7 @@ export function useAuthFlow(options = {}) {
emailAuth.value.isLoading = true;
try {
const response = await api.post('/api/auth/email/init', { email: emailAuth.value.email });
const response = await api.post('/auth/email/init', { email: emailAuth.value.email });
if (response.data.success) {
emailAuth.value.verificationEmail = emailAuth.value.email; // Сохраняем email
emailAuth.value.showForm = false;
@@ -161,7 +161,7 @@ export function useAuthFlow(options = {}) {
emailAuth.value.isVerifying = true;
try {
const response = await api.post('/api/auth/email/verify-code', {
const response = await api.post('/auth/email/verify-code', {
email: emailAuth.value.verificationEmail,
code: emailAuth.value.verificationCode,
});

View File

@@ -14,7 +14,7 @@ export default function useBlockchainNetworks() {
const fetchNetworks = async () => {
loadingNetworks.value = true;
try {
const { data } = await axios.get('/api/settings/rpc');
const { data } = await axios.get('/settings/rpc');
const networksArr = data.data || [];
networks.value = networksArr.map(n => ({
value: n.network_id,
@@ -163,7 +163,7 @@ export default function useBlockchainNetworks() {
try {
// Формируем запрос на бэкенд для проверки RPC
const response = await axios.post('/api/settings/rpc-test', {
const response = await axios.post('/settings/rpc-test', {
networkId,
rpcUrl
});

View File

@@ -80,7 +80,7 @@ export function useChat(auth) {
let totalMessages = -1;
if (initial || messageLoading.value.offset === 0) {
try {
const countResponse = await api.get('/api/chat/history', { params: { count_only: true } });
const countResponse = await api.get('/chat/history', { params: { count_only: true } });
if (!countResponse.data.success) throw new Error('Не удалось получить количество сообщений');
totalMessages = countResponse.data.total || countResponse.data.count || 0;
console.log(`[useChat] Всего сообщений в истории: ${totalMessages}`);
@@ -97,7 +97,7 @@ export function useChat(auth) {
console.log(`[useChat] Рассчитано начальное смещение: ${effectiveOffset}`);
}
const response = await api.get('/api/chat/history', {
const response = await api.get('/chat/history', {
params: {
offset: effectiveOffset,
limit: messageLoading.value.limit,
@@ -225,14 +225,14 @@ export function useChat(auth) {
});
}
let apiUrl = '/api/chat/message';
let apiUrl = '/chat/message';
if (isGuestMessage) {
if (!guestId.value) {
guestId.value = initGuestId();
setToStorage('guestId', guestId.value);
}
formData.append('guestId', guestId.value);
apiUrl = '/api/chat/guest-message';
apiUrl = '/chat/guest-message';
}
const response = await api.post(apiUrl, formData, {
@@ -352,7 +352,7 @@ export function useChat(auth) {
const linkGuestMessagesAfterAuth = async () => {
if (!guestId.value) return;
try {
const response = await api.post('/api/chat/process-guest', { guestId: guestId.value });
const response = await api.post('/chat/process-guest', { guestId: guestId.value });
if (response.data.success && response.data.conversationId) {
// Можно сразу загрузить историю по этому диалогу, если нужно
await loadMessages({ initial: true });

View File

@@ -42,7 +42,7 @@ export function useContactsAndMessagesWebSocket() {
async function fetchContactsReadStatus() {
try {
const { data } = await axios.get('/api/users/read-contacts-status');
const { data } = await axios.get('/users/read-contacts-status');
readContacts.value = data || [];
} catch (e) {
readContacts.value = [];
@@ -60,7 +60,7 @@ export function useContactsAndMessagesWebSocket() {
async function markContactAsRead(contactId) {
try {
await axios.post('/api/users/mark-contact-read', { contactId });
await axios.post('/users/mark-contact-read', { contactId });
if (!readContacts.value.includes(contactId)) {
readContacts.value.push(contactId);
updateNewContacts();
@@ -70,7 +70,7 @@ export function useContactsAndMessagesWebSocket() {
async function fetchReadStatus() {
try {
const { data } = await axios.get('/api/messages/read-status');
const { data } = await axios.get('/messages/read-status');
lastReadMessageDate.value = data || {};
} catch (e) {
lastReadMessageDate.value = {};
@@ -97,7 +97,7 @@ export function useContactsAndMessagesWebSocket() {
const maxDate = Math.max(...userMessages.map(m => new Date(m.created_at).getTime()));
const maxDateISO = new Date(maxDate).toISOString();
try {
await axios.post('/api/messages/mark-read', { userId, lastReadAt: maxDateISO });
await axios.post('/messages/mark-read', { userId, lastReadAt: maxDateISO });
lastReadMessageDate.value[userId] = maxDateISO;
} catch (e) {}
}
@@ -114,7 +114,8 @@ export function useContactsAndMessagesWebSocket() {
}
function setupWebSocket() {
ws = new WebSocket('ws://localhost:8000');
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'contacts-updated') {

View File

@@ -9,12 +9,7 @@ import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
// Настройка axios
// В Docker контейнере localhost:8000 не работает, поэтому используем явное значение
const apiUrl =
window.location.hostname === 'localhost'
? 'http://localhost:8000'
: 'http://dapp-backend:8000'; // имя контейнера backend
axios.defaults.baseURL = apiUrl;
axios.defaults.baseURL = '/api';
axios.defaults.withCredentials = true;
// Создаем и монтируем приложение Vue
@@ -33,7 +28,7 @@ app.use(ElementPlus);
// ]).catch(err => console.error('Failed to load API mocks:', err));
// }
console.log('API URL:', apiUrl);
console.log('API URL:', axios.defaults.baseURL);
console.log('main.js: Starting application with router');
app.mount('#app');

View File

@@ -53,6 +53,11 @@ const routes = [
name: 'settings-interface',
component: SettingsInterfaceView,
},
{
path: 'webssh',
name: 'settings-webssh',
component: () => import('../views/settings/WebSshSettingsView.vue'),
},
{
path: 'telegram',
@@ -170,7 +175,7 @@ router.beforeEach(async (to, from, next) => {
// Проверяем аутентификацию, если маршрут требует авторизации
if (to.matched.some((record) => record.meta.requiresAuth)) {
try {
const response = await axios.get('/api/auth/check');
const response = await axios.get('/auth/check');
if (response.data.authenticated) {
next();
} else {

View File

@@ -2,19 +2,19 @@ import api from '../api/axios';
export default {
async getContacts() {
const res = await api.get('/api/users');
const res = await api.get('/users');
if (res.data && res.data.success) {
return res.data.contacts;
}
return [];
},
async updateContact(id, data) {
const res = await api.patch(`/api/users/${id}`, data);
const res = await api.patch(`/users/${id}`, data);
return res.data;
},
async deleteContact(id) {
try {
const res = await api.delete(`/api/users/${id}`);
const res = await api.delete(`/users/${id}`);
console.log('Ответ на удаление контакта:', res.status, res.data);
return res.data;
} catch (err) {
@@ -23,18 +23,18 @@ export default {
}
},
async getContactById(id) {
const res = await api.get(`/api/users/${id}`);
const res = await api.get(`/users/${id}`);
if (res.data && res.data.id) {
return res.data;
}
return null;
},
async blockContact(id) {
const res = await api.patch(`/api/users/${id}/block`);
const res = await api.patch(`/users/${id}/block`);
return res.data;
},
async unblockContact(id) {
const res = await api.patch(`/api/users/${id}/unblock`);
const res = await api.patch(`/users/${id}/unblock`);
return res.data;
}
};

View File

@@ -10,7 +10,7 @@ class DLEService {
*/
async getDefaultSettings() {
try {
const response = await api.get('/api/dle/settings');
const response = await api.get('/dle/settings');
return response.data.data;
} catch (error) {
console.error('Ошибка при получении настроек DLE:', error);
@@ -25,7 +25,7 @@ class DLEService {
*/
async createDLE(dleParams) {
try {
const response = await api.post('/api/dle', dleParams);
const response = await api.post('/dle', dleParams);
return response.data;
} catch (error) {
console.error('Ошибка при создании DLE:', error);
@@ -39,7 +39,7 @@ class DLEService {
*/
async getAllDLEs() {
try {
const response = await api.get('/api/dle');
const response = await api.get('/dle');
// Проверяем и нормализуем поля isicCodes для всех DLE
if (response.data.data && Array.isArray(response.data.data)) {
@@ -65,7 +65,7 @@ class DLEService {
*/
async deleteDLE(tokenAddress) {
try {
const response = await api.delete(`/api/dle/${tokenAddress}`);
const response = await api.delete(`/dle/${tokenAddress}`);
return response.data;
} catch (error) {
console.error('Ошибка при удалении DLE:', error);
@@ -80,7 +80,7 @@ class DLEService {
*/
async deleteEmptyDLE(fileName) {
try {
const response = await api.delete(`/api/dle/empty/${fileName}`);
const response = await api.delete(`/dle/empty/${fileName}`);
return response.data;
} catch (error) {
console.error('Ошибка при удалении пустого DLE:', error);

View File

@@ -3,7 +3,7 @@ import axios from 'axios';
export default {
async getMessagesByUserId(userId) {
if (!userId) return [];
const { data } = await axios.get(`/api/messages?userId=${userId}`);
const { data } = await axios.get(`/messages?userId=${userId}`);
return data;
},
async sendMessage({ conversationId, message, attachments = [], toUserId }) {
@@ -14,7 +14,7 @@ export default {
attachments.forEach(file => {
formData.append('attachments', file);
});
const { data } = await axios.post('/api/chat/message', formData, {
const { data } = await axios.post('/chat/message', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
withCredentials: true
});
@@ -22,20 +22,20 @@ export default {
},
async getMessagesByConversationId(conversationId) {
if (!conversationId) return [];
const { data } = await axios.get(`/api/messages?conversationId=${conversationId}`);
const { data } = await axios.get(`/messages?conversationId=${conversationId}`);
return data;
},
async getConversationByUserId(userId) {
if (!userId) return null;
const { data } = await axios.get(`/api/messages/conversations?userId=${userId}`);
const { data } = await axios.get(`/messages/conversations?userId=${userId}`);
return data;
},
async generateAiDraft(conversationId, messages, language = 'auto') {
const { data } = await axios.post('/api/chat/ai-draft', { conversationId, messages, language });
const { data } = await axios.post('/chat/ai-draft', { conversationId, messages, language });
return data;
},
async broadcastMessage({ userId, message }) {
const { data } = await axios.post('/api/messages/broadcast', {
const { data } = await axios.post('/messages/broadcast', {
user_id: userId,
content: message
}, {
@@ -46,6 +46,6 @@ export default {
};
export async function getAllMessages() {
const { data } = await axios.get('/api/messages');
const { data } = await axios.get('/messages');
return data;
}

View File

@@ -1,6 +1,6 @@
import api from '../api/axios';
const tablesApi = '/api/tables';
const tablesApi = '/tables';
export default {
async getTables() {

View File

@@ -3,7 +3,7 @@ import api from '../api/axios';
// Получение балансов токенов
export const fetchTokenBalances = async (address = null) => {
try {
let url = '/api/tokens/balances';
let url = '/tokens/balances';
if (address) {
url += `?address=${encodeURIComponent(address)}`;
console.log(`Fetching token balances for specific address: ${address}`);

View File

@@ -28,7 +28,7 @@ export async function connectWithWallet() {
// Запрашиваем nonce с сервера
console.log('Requesting nonce...');
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`);
const nonceResponse = await axios.get(`/auth/nonce?address=${address}`);
const nonce = nonceResponse.data.nonce;
console.log('Got nonce:', nonce);
@@ -62,7 +62,7 @@ export async function connectWithWallet() {
// Отправляем подпись на сервер для верификации
console.log('Sending verification request...');
const verificationResponse = await axios.post('/api/auth/verify', {
const verificationResponse = await axios.post('/auth/verify', {
message,
signature,
address,

View File

@@ -0,0 +1,475 @@
/**
* Сервис для управления WEB SSH туннелем
* Взаимодействует с локальным агентом на порту 12345
*/
const LOCAL_AGENT_URL = 'http://localhost:12345';
class WebSshService {
constructor() {
this.isAgentRunning = false;
this.connectionStatus = {
connected: false,
domain: null,
tunnelId: null
};
}
/**
* Проверка доступности локального агента
*/
async checkAgentStatus() {
try {
const response = await fetch(`${LOCAL_AGENT_URL}/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
this.isAgentRunning = true;
return { running: true };
}
this.isAgentRunning = false;
return { running: false };
} catch (error) {
console.error('Агент не доступен:', error);
this.isAgentRunning = false;
return { running: false, error: error.message };
}
}
/**
* Автоматическая установка и запуск агента
*/
async installAndStartAgent() {
try {
// Сначала проверяем, может агент уже запущен
const status = await this.checkAgentStatus();
if (status.running) {
return { success: true, message: 'Агент уже запущен' };
}
// Пытаемся запустить агент через системный вызов
const response = await fetch(`${LOCAL_AGENT_URL}/install`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'install_and_start'
})
});
if (response.ok) {
const result = await response.json();
this.isAgentRunning = true;
return { success: true, message: 'Агент успешно установлен и запущен' };
} else {
// Если агент не отвечает, пытаемся скачать и установить его
return await this.downloadAndInstallAgent();
}
} catch (error) {
console.error('Ошибка при установке агента:', error);
return await this.downloadAndInstallAgent();
}
}
/**
* Скачивание и установка агента
*/
async downloadAndInstallAgent() {
try {
// Создаем скрипт для скачивания и установки агента
const installScript = `
#!/bin/bash
# Создаем директорию для агента
mkdir -p ~/.webssh-agent
cd ~/.webssh-agent
# Скачиваем агент (пока создаем локально)
cat > agent.js << 'EOF'
${this.getAgentCode()}
EOF
# Скачиваем package.json
cat > package.json << 'EOF'
{
"name": "webssh-agent",
"version": "1.0.0",
"description": "Local SSH tunnel agent",
"main": "agent.js",
"scripts": {
"start": "node agent.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"ssh2": "^1.14.0",
"node-ssh": "^13.1.0"
}
}
EOF
# Устанавливаем зависимости
npm install
# Запускаем агент в фоне
nohup node agent.js > agent.log 2>&1 &
echo "Агент установлен и запущен"
`;
// Создаем Blob со скриптом
const blob = new Blob([installScript], { type: 'application/x-sh' });
const url = URL.createObjectURL(blob);
// Создаем ссылку для скачивания
const a = document.createElement('a');
a.href = url;
a.download = 'install-webssh-agent.sh';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
return {
success: false,
message: 'Скачайте и запустите скрипт install-webssh-agent.sh для установки агента',
requiresManualInstall: true
};
} catch (error) {
console.error('Ошибка при создании установочного скрипта:', error);
return {
success: false,
message: 'Ошибка при подготовке установки агента',
error: error.message
};
}
}
/**
* Создание SSH туннеля
*/
async createTunnel(config) {
try {
// Проверяем, что агент запущен
const agentStatus = await this.checkAgentStatus();
if (!agentStatus.running) {
// Пытаемся установить и запустить агент
const installResult = await this.installAndStartAgent();
if (!installResult.success) {
return installResult;
}
}
// Отправляем конфигурацию туннеля агенту
const response = await fetch(`${LOCAL_AGENT_URL}/tunnel/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
domain: config.domain,
email: config.email,
sshHost: config.sshHost,
sshUser: config.sshUser,
sshKey: config.sshKey,
localPort: config.localPort || 5173,
serverPort: config.serverPort || 9000,
sshPort: config.sshPort || 22
})
});
if (response.ok) {
const result = await response.json();
if (result.success) {
this.connectionStatus = {
connected: true,
domain: config.domain,
tunnelId: result.tunnelId
};
}
return result;
} else {
const error = await response.json();
return {
success: false,
message: error.message || 'Ошибка при создании туннеля'
};
}
} catch (error) {
console.error('Ошибка при создании туннеля:', error);
return {
success: false,
message: `Ошибка подключения к агенту: ${error.message}`
};
}
}
/**
* Отключение туннеля
*/
async disconnectTunnel() {
try {
const response = await fetch(`${LOCAL_AGENT_URL}/tunnel/disconnect`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tunnelId: this.connectionStatus.tunnelId
})
});
if (response.ok) {
const result = await response.json();
if (result.success) {
this.connectionStatus = {
connected: false,
domain: null,
tunnelId: null
};
}
return result;
} else {
const error = await response.json();
return {
success: false,
message: error.message || 'Ошибка при отключении туннеля'
};
}
} catch (error) {
console.error('Ошибка при отключении туннеля:', error);
return {
success: false,
message: `Ошибка подключения к агенту: ${error.message}`
};
}
}
/**
* Получение статуса подключения
*/
async getStatus() {
try {
const response = await fetch(`${LOCAL_AGENT_URL}/tunnel/status`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
this.connectionStatus = result;
return result;
} else {
return {
connected: false,
domain: null,
tunnelId: null
};
}
} catch (error) {
console.error('Ошибка при получении статуса:', error);
return {
connected: false,
domain: null,
tunnelId: null
};
}
}
/**
* Получение кода агента для установки
*/
getAgentCode() {
return `
const express = require('express');
const cors = require('cors');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const { NodeSSH } = require('node-ssh');
const app = express();
const PORT = 12345;
// Middleware
app.use(cors());
app.use(express.json());
// Состояние туннеля
let tunnelState = {
connected: false,
domain: null,
tunnelId: null,
sshProcess: null
};
// Здоровье агента
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Создание туннеля
app.post('/tunnel/create', async (req, res) => {
try {
const { domain, email, sshHost, sshUser, sshKey, localPort, serverPort, sshPort } = req.body;
console.log('Создание туннеля для домена:', domain);
// Сохраняем SSH ключ во временный файл
const keyPath = path.join(__dirname, 'temp_ssh_key');
fs.writeFileSync(keyPath, sshKey, { mode: 0o600 });
// Подключаемся к серверу и настраиваем NGINX
const ssh = new NodeSSH();
await ssh.connect({
host: sshHost,
username: sshUser,
privateKey: sshKey,
port: sshPort
});
// Установка NGINX и certbot
await ssh.execCommand('apt-get update && apt-get install -y nginx certbot python3-certbot-nginx');
// Создание конфигурации NGINX
const nginxConfig = \`
server {
listen 80;
server_name \${domain};
location / {
proxy_pass http://localhost:\${serverPort};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
\`;
await ssh.execCommand(\`echo '\${nginxConfig}' > /etc/nginx/sites-available/\${domain}\`);
await ssh.execCommand(\`ln -sf /etc/nginx/sites-available/\${domain} /etc/nginx/sites-enabled/\`);
await ssh.execCommand('nginx -t && systemctl reload nginx');
// Получение SSL сертификата
await ssh.execCommand(\`certbot --nginx -d \${domain} --non-interactive --agree-tos --email \${email}\`);
ssh.dispose();
// Создание SSH туннеля
const tunnelId = Date.now().toString();
const sshArgs = [
'-i', keyPath,
'-p', sshPort.toString(),
'-R', \`\${serverPort}:localhost:\${localPort}\`,
'-N',
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
\`\${sshUser}@\${sshHost}\`
];
const sshProcess = spawn('ssh', sshArgs);
sshProcess.on('error', (error) => {
console.error('SSH процесс ошибка:', error);
});
sshProcess.on('close', (code) => {
console.log('SSH процесс завершен с кодом:', code);
tunnelState.connected = false;
});
// Обновляем состояние
tunnelState = {
connected: true,
domain,
tunnelId,
sshProcess
};
res.json({
success: true,
message: 'Туннель успешно создан',
tunnelId,
domain
});
} catch (error) {
console.error('Ошибка создания туннеля:', error);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Отключение туннеля
app.post('/tunnel/disconnect', (req, res) => {
try {
if (tunnelState.sshProcess) {
tunnelState.sshProcess.kill();
}
tunnelState = {
connected: false,
domain: null,
tunnelId: null,
sshProcess: null
};
res.json({
success: true,
message: 'Туннель отключен'
});
} catch (error) {
console.error('Ошибка отключения туннеля:', error);
res.status(500).json({
success: false,
message: error.message
});
}
});
// Статус туннеля
app.get('/tunnel/status', (req, res) => {
res.json({
connected: tunnelState.connected,
domain: tunnelState.domain,
tunnelId: tunnelState.tunnelId
});
});
// Запуск сервера
app.listen(PORT, 'localhost', () => {
console.log(\`WebSSH Agent запущен на порту \${PORT}\`);
});
`;
}
}
// Создаем композабл для использования в компонентах
export function useWebSshService() {
const service = new WebSshService();
return {
checkAgentStatus: () => service.checkAgentStatus(),
installAndStartAgent: () => service.installAndStartAgent(),
createTunnel: (config) => service.createTunnel(config),
disconnectTunnel: () => service.disconnectTunnel(),
getStatus: () => service.getStatus()
};
}
export default WebSshService;

86
frontend/src/utils/api.js Normal file
View File

@@ -0,0 +1,86 @@
/**
* Безопасный fetch с проверкой JSON-ответа
* @param {string} url - URL для запроса
* @param {object} options - Опции для fetch
* @returns {Promise<any>} - Распарсенный JSON или ошибка
*/
export async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// Проверяем статус ответа
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Проверяем Content-Type
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
// Если это HTML, значит, запрос ушёл не на API
if (contentType && contentType.includes('text/html')) {
throw new Error('Сервер вернул HTML вместо JSON. Проверьте путь к API.');
}
throw new Error(`Неожиданный Content-Type: ${contentType}`);
}
// Парсим JSON
const data = await response.json();
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
/**
* Обёртка для GET-запросов
* @param {string} url - URL для запроса
* @returns {Promise<any>} - Распарсенный JSON
*/
export async function apiGet(url) {
return safeFetch(url);
}
/**
* Обёртка для POST-запросов
* @param {string} url - URL для запроса
* @param {object} data - Данные для отправки
* @returns {Promise<any>} - Распарсенный JSON
*/
export async function apiPost(url, data) {
return safeFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
}
/**
* Обёртка для PUT-запросов
* @param {string} url - URL для запроса
* @param {object} data - Данные для отправки
* @returns {Promise<any>} - Распарсенный JSON
*/
export async function apiPut(url, data) {
return safeFetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
}
/**
* Обёртка для DELETE-запросов
* @param {string} url - URL для запроса
* @returns {Promise<any>} - Распарсенный JSON
*/
export async function apiDelete(url) {
return safeFetch(url, {
method: 'DELETE',
});
}

View File

@@ -37,7 +37,7 @@ export const connectWallet = async () => {
// Запрашиваем nonce с сервера
console.log('Requesting nonce...');
const nonceResponse = await axios.get(`/api/auth/nonce?address=${normalizedAddress}`);
const nonceResponse = await axios.get(`/auth/nonce?address=${normalizedAddress}`);
const nonce = nonceResponse.data.nonce;
console.log('Got nonce:', nonce);
@@ -88,7 +88,7 @@ export const connectWallet = async () => {
// Отправляем верификацию на сервер
console.log('Sending verification request...');
const verifyResponse = await axios.post('/api/auth/verify', {
const verifyResponse = await axios.post('/auth/verify', {
address: normalizedAddress,
signature,
nonce,

View File

@@ -70,7 +70,8 @@ let ws = null;
function connectWebSocket() {
if (ws) ws.close();
ws = new WebSocket('ws://localhost:8000');
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
ws = new WebSocket(`${wsProtocol}://${window.location.host}/ws`);
ws.onopen = () => {
console.log('[CRM] WebSocket соединение установлено');
};

View File

@@ -40,8 +40,7 @@ async function reloadDleList() {
}
onMounted(async () => {
dleList.value = await dleService.getAllDLEs() || [];
onMounted(reloadDleList);
await reloadDleList();
});
</script>

View File

@@ -27,8 +27,6 @@
@send-message="handleSendMessage"
@load-more="loadMessages"
/>
<!-- Можно добавить заглушку или пояснение -->
<div class="empty-table-placeholder">Вы видите только свои сообщения. Данные других пользователей недоступны.</div>
</template>
</BaseLayout>
</template>

View File

@@ -87,33 +87,33 @@ const filteredEmbeddingModels = computed(() => {
return embeddingModels.value.filter(m => m.provider === selectedLLM.value.provider);
});
async function loadUserTables() {
const { data } = await axios.get('/api/tables');
const { data } = await axios.get('/tables');
userTables.value = Array.isArray(data) ? data : [];
}
async function loadRules() {
const { data } = await axios.get('/api/settings/ai-assistant-rules');
const { data } = await axios.get('/settings/ai-assistant-rules');
rulesList.value = data.rules || [];
}
async function loadSettings() {
const { data } = await axios.get('/api/settings/ai-assistant');
const { data } = await axios.get('/settings/ai-assistant');
if (data.success && data.settings) {
settings.value = data.settings;
}
}
async function loadTelegramBots() {
const { data } = await axios.get('/api/settings/telegram-settings/list');
const { data } = await axios.get('/settings/telegram-settings/list');
telegramBots.value = data.items || [];
}
async function loadEmailList() {
const { data } = await axios.get('/api/settings/email-settings/list');
const { data } = await axios.get('/settings/email-settings/list');
emailList.value = data.items || [];
}
async function loadLLMModels() {
const { data } = await axios.get('/api/settings/llm-models');
const { data } = await axios.get('/settings/llm-models');
llmModels.value = data.models || [];
}
async function loadEmbeddingModels() {
const { data } = await axios.get('/api/settings/embedding-models');
const { data } = await axios.get('/settings/embedding-models');
embeddingModels.value = data.models || [];
}
onMounted(() => {
@@ -126,7 +126,7 @@ onMounted(() => {
loadEmbeddingModels();
});
async function saveSettings() {
await axios.put('/api/settings/ai-assistant', settings.value);
await axios.put('/settings/ai-assistant', settings.value);
goBack();
}
function openRuleEditor(ruleId = null) {
@@ -139,7 +139,7 @@ function openRuleEditor(ruleId = null) {
}
async function deleteRule(ruleId) {
if (!confirm('Удалить этот набор правил?')) return;
await axios.delete(`/api/settings/ai-assistant-rules/${ruleId}`);
await axios.delete(`/settings/ai-assistant-rules/${ruleId}`);
await loadRules();
if (settings.value.rules_id === ruleId) settings.value.rules_id = null;
}

View File

@@ -63,7 +63,7 @@ const editMode = ref(false);
const loadDbSettings = async () => {
try {
const res = await api.get('/api/settings/db-settings');
const res = await api.get('/settings/db-settings');
if (res.data.success) {
const s = res.data.settings;
form.dbHost = s.db_host;
@@ -85,7 +85,7 @@ onMounted(async () => {
const saveDbSettings = async () => {
try {
await api.put('/api/settings/db-settings', {
await api.put('/settings/db-settings', {
db_host: form.dbHost,
db_port: form.dbPort,
db_name: form.dbName,

View File

@@ -74,7 +74,7 @@ const editMode = ref(false);
const loadEmailSettings = async () => {
try {
const res = await api.get('/api/settings/email-settings');
const res = await api.get('/settings/email-settings');
if (res.data.success) {
const s = res.data.settings;
form.smtpHost = s.smtp_host;
@@ -98,7 +98,7 @@ onMounted(async () => {
const saveEmailSettings = async () => {
try {
await api.put('/api/settings/email-settings', {
await api.put('/settings/email-settings', {
smtp_host: form.smtpHost,
smtp_port: form.smtpPort,
smtp_user: form.smtpUser,

View File

@@ -45,7 +45,7 @@ const editMode = ref(false);
const loadTelegramSettings = async () => {
try {
const res = await api.get('/api/settings/telegram-settings');
const res = await api.get('/settings/telegram-settings');
if (res.data.success) {
const s = res.data.settings;
form.botToken = s.bot_token || '';
@@ -64,7 +64,7 @@ onMounted(async () => {
const saveTelegramSettings = async () => {
try {
await api.put('/api/telegram-settings', {
await api.put('/telegram-settings', {
bot_token: form.botToken,
bot_username: form.botUsername
});

View File

@@ -70,7 +70,7 @@ const saveError = ref('');
async function loadSettings() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}`);
const { data } = await axios.get(`/settings/ai-settings/${props.provider}`);
if (data.settings) {
apiKey.value = data.settings.api_key || '';
baseUrl.value = data.settings.base_url || '';
@@ -91,7 +91,7 @@ async function loadSettings() {
async function loadModels() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
models.value = data.models || [];
if (!selectedModel.value && models.value.length) {
const first = models.value[0];
@@ -104,7 +104,7 @@ async function loadModels() {
async function loadEmbeddingModels() {
try {
const { data } = await axios.get(`/api/settings/ai-settings/${props.provider}/models`);
const { data } = await axios.get(`/settings/ai-settings/${props.provider}/models`);
embeddingModels.value = (data.models || []).filter(m => {
const name = m.id || m.name || m;
return name && name.toLowerCase().includes('embed');
@@ -123,7 +123,7 @@ async function onVerify() {
verifyStatus.value = null;
verifyError.value = '';
try {
const { data } = await axios.post(`/api/settings/ai-settings/${props.provider}/verify`, {
const { data } = await axios.post(`/settings/ai-settings/${props.provider}/verify`, {
api_key: apiKey.value,
base_url: baseUrl.value,
});
@@ -144,7 +144,7 @@ async function onSave() {
saveStatus.value = null;
saveError.value = '';
try {
await axios.put(`/api/settings/ai-settings/${props.provider}`, {
await axios.put(`/settings/ai-settings/${props.provider}`, {
api_key: apiKey.value,
base_url: baseUrl.value,
selected_model: selectedModel.value,
@@ -161,7 +161,7 @@ async function onSave() {
}
async function onDelete() {
await axios.delete(`/api/settings/ai-settings/${props.provider}`);
await axios.delete(`/settings/ai-settings/${props.provider}`);
apiKey.value = '';
baseUrl.value = '';
selectedModel.value = '';

View File

@@ -93,7 +93,7 @@ async function addToken() {
return;
}
try {
await api.post('/api/settings/auth-token', {
await api.post('/settings/auth-token', {
...newToken,
minBalance: Number(newToken.minBalance) || 0
});

View File

@@ -446,7 +446,7 @@ const fetchIsicCodes = async (params = {}, optionsRef, loadingRef) => {
console.debug(`[BlockchainSettingsView] Fetching ISIC codes with params: ${queryParams}`);
// Убедитесь, что базовый URL настроен правильно (например, через axios interceptors или .env)
const response = await axios.get(`/api/isic/codes?${queryParams}`);
const response = await axios.get(`/isic/codes?${queryParams}`);
if (response.data && Array.isArray(response.data.codes)) {
optionsRef.value = response.data.codes.map(code => ({
@@ -716,7 +716,7 @@ const fetchAddressByZipcode = async () => {
}
console.log(`[FetchByZipcode] Querying backend proxy for Nominatim with: ${params.toString()}`);
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
if (response.data && response.data.length > 0) {
const bestMatch = response.data[0];
@@ -818,7 +818,7 @@ const verifyAddress = async () => {
console.log(`[VerifyAddress] Querying backend proxy for Nominatim with: ${params.toString()}`);
// Запрос теперь идет на ваш бэкенд-прокси
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
// Ответ от бэкенд-прокси должен иметь ту же структуру, что и прямой ответ от Nominatim
if (response.data && Array.isArray(response.data)) { // Проверяем, что это массив (как отвечает Nominatim)
@@ -901,7 +901,7 @@ const toggleShowDeployerKey = () => {
// Функция загрузки настроек RPC с сервера
const loadRpcSettings = async () => {
try {
const response = await axios.get('/api/settings/rpc');
const response = await axios.get('/settings/rpc');
console.log('Ответ сервера на /api/settings/rpc:', response.data);
if (response.data && response.data.success) {
securitySettings.rpcConfigs = (response.data.data || []).map(rpc => ({
@@ -921,7 +921,7 @@ const loadRpcSettings = async () => {
const saveRpcSettings = async () => {
try {
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
const response = await axios.post('/api/settings/rpc', {
const response = await axios.post('/settings/rpc', {
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
});

View File

@@ -73,7 +73,7 @@ async function addRpc() {
return;
}
try {
await api.post('/api/settings/rpc', { networkId, rpcUrl, chainId });
await api.post('/settings/rpc', { networkId, rpcUrl, chainId });
emit('update'); // сигнал родителю перезагрузить список
resetNetworkEntry();
} catch (e) {

View File

@@ -138,7 +138,7 @@ const loadSettings = async () => {
isLoading.value = true;
try {
// Загрузка RPC конфигураций
const rpcResponse = await api.get('/api/settings/rpc');
const rpcResponse = await api.get('/settings/rpc');
if (rpcResponse.data && rpcResponse.data.success) {
securitySettings.rpcConfigs = (rpcResponse.data.data || []).map(rpc => ({
networkId: rpc.network_id,
@@ -149,7 +149,7 @@ const loadSettings = async () => {
}
// Загрузка токенов для аутентификации
const authResponse = await api.get('/api/settings/auth-tokens');
const authResponse = await api.get('/settings/auth-tokens');
if (authResponse.data && authResponse.data.success) {
securitySettings.authTokens = (authResponse.data.data || []).map(token => ({
name: token.name,
@@ -195,12 +195,12 @@ const saveSecuritySettings = async () => {
console.log('[SecuritySettingsView] Отправка настроек на сервер:', settingsData);
// Отправка RPC конфигураций
const rpcResponse = await api.post('/api/settings/rpc', {
const rpcResponse = await api.post('/settings/rpc', {
rpcConfigs: settingsData.rpcConfigs
});
// Отправка токенов для аутентификации
const authResponse = await api.post('/api/settings/auth-tokens', {
const authResponse = await api.post('/settings/auth-tokens', {
authTokens: settingsData.authTokens
});

View File

@@ -20,6 +20,11 @@
<p>Настройки внешнего вида, локализации и пользовательского опыта.</p>
<button class="details-btn" @click="$router.push('/settings/interface')">Подробнее</button>
</div>
<div class="main-block">
<h3>WEB SSH</h3>
<p>Автоматическая публикация приложения в интернете через SSH-туннель.</p>
<button class="details-btn" @click="$router.push('/settings/webssh')">Подробнее</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,703 @@
<template>
<div class="web-ssh-settings">
<div class="settings-header">
<h2>WEB SSH Туннель</h2>
<p>Автоматическая публикация локального приложения в интернете через SSH-туннель и NGINX</p>
</div>
<div v-if="!agentAvailable" class="agent-instruction-block">
<h3>Установка локального агента</h3>
<ol>
<li>
<b>Windows:</b><br>
Откройте <b>PowerShell</b> или <b>Командную строку</b> и выполните:
<div class="copy-block" @click="copyToClipboard('wsl')">
<pre><code>wsl</code></pre>
<span class="copy-icon">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
Затем в открывшемся терминале WSL выполните:
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
<pre><code>cd ~/DApp-for-Business
sudo bash webssh-agent/install.sh</code></pre>
<span class="copy-icon">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</li>
<li>
<b>Linux:</b><br>
Откройте терминал и выполните:
<div class="copy-block" @click="copyToClipboard('cd ~/DApp-for-Business\nsudo bash webssh-agent/install.sh')">
<pre><code>cd ~/DApp-for-Business
sudo bash webssh-agent/install.sh</code></pre>
<span class="copy-icon">
<svg v-if="!copied" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="5" y="7" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/><rect x="7" y="4" width="9" height="9" rx="2" stroke="#888" stroke-width="1.5"/></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M5 11.5L9 15L15 7" stroke="#27ae60" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</div>
</li>
</ol>
<button @click="checkAgent" class="check-btn">Проверить</button>
<div v-if="copied" class="copied-indicator">Скопировано!</div>
</div>
<!-- Форма публикации всегда доступна -->
<div>
<!-- Статус подключения -->
<div class="connection-status">
<div class="status-indicator" :class="{ 'active': isConnected, 'inactive': !isConnected }"></div>
<span class="status-text">{{ connectionStatus }}</span>
<button v-if="isConnected" @click="disconnectTunnel" class="disconnect-btn">Отключить</button>
</div>
<!-- Форма настроек -->
<form @submit.prevent="handleSubmit" class="tunnel-form">
<div class="form-section">
<h3>Настройки домена</h3>
<div class="form-group">
<label for="domain">Домен *</label>
<input
id="domain"
v-model="form.domain"
type="text"
placeholder="example.com"
required
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="email">Email для SSL *</label>
<input
id="email"
v-model="form.email"
type="email"
placeholder="admin@example.com"
required
:disabled="isConnected"
/>
</div>
</div>
<div class="form-section">
<h3>Настройки SSH сервера</h3>
<div class="form-group">
<label for="sshHost">SSH Host/IP *</label>
<input
id="sshHost"
v-model="form.sshHost"
type="text"
placeholder="192.168.1.100 или server.example.com"
required
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="sshUser">SSH Пользователь *</label>
<input
id="sshUser"
v-model="form.sshUser"
type="text"
placeholder="root"
required
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="sshKey">SSH Приватный ключ *</label>
<textarea
id="sshKey"
v-model="form.sshKey"
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----"
rows="6"
required
:disabled="isConnected"
></textarea>
</div>
</div>
<div class="form-section advanced-section">
<h3>Дополнительные настройки</h3>
<div class="form-row">
<div class="form-group">
<label for="localPort">Локальный порт</label>
<input
id="localPort"
v-model="form.localPort"
type="number"
min="1"
max="65535"
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="serverPort">Порт сервера</label>
<input
id="serverPort"
v-model="form.serverPort"
type="number"
min="1"
max="65535"
:disabled="isConnected"
/>
</div>
<div class="form-group">
<label for="sshPort">SSH порт</label>
<input
id="sshPort"
v-model="form.sshPort"
type="number"
min="1"
max="65535"
:disabled="isConnected"
/>
</div>
</div>
</div>
<div class="form-actions">
<button
type="submit"
:disabled="isLoading || isConnected"
class="publish-btn"
>
{{ isLoading ? 'Настройка...' : 'Опубликовать' }}
</button>
<button
type="button"
@click="resetForm"
:disabled="isLoading || isConnected"
class="reset-btn"
>
Сбросить
</button>
</div>
</form>
<!-- Лог операций -->
<div class="operation-log" v-if="logs.length > 0">
<h3>Лог операций</h3>
<div class="log-container">
<div
v-for="(log, index) in logs"
:key="index"
class="log-entry"
:class="log.type"
>
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { useWebSshService } from '../../services/webSshService';
const webSshService = useWebSshService();
const agentAvailable = ref(false);
// Реактивные данные
const isLoading = ref(false);
const isConnected = ref(false);
const connectionStatus = ref('Не подключено');
const logs = ref([]);
// Форма
const form = reactive({
domain: '',
email: '',
sshHost: '',
sshUser: '',
sshKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
});
const copied = ref(false);
let copyTimeout = null;
function copyToClipboard(text) {
navigator.clipboard.writeText(text.replace(/\\n/g, '\n')).then(() => {
copied.value = true;
clearTimeout(copyTimeout);
copyTimeout = setTimeout(() => copied.value = false, 1200);
});
}
function validatePrivateKey(key) {
if (!key) return false;
const trimmed = key.trim();
if (!trimmed.startsWith('-----BEGIN OPENSSH PRIVATE KEY-----')) {
console.error('Ключ не начинается с -----BEGIN OPENSSH PRIVATE KEY-----');
return false;
}
if (!trimmed.endsWith('-----END OPENSSH PRIVATE KEY-----')) {
console.error('Ключ не заканчивается на -----END OPENSSH PRIVATE KEY-----');
return false;
}
if (trimmed.split('\n').length < 3) {
console.error('Ключ слишком короткий или не содержит переносов строк');
return false;
}
return true;
}
// Методы
const handleSubmit = async () => {
if (!validateForm()) return;
// Дополнительная валидация приватного ключа
if (!validatePrivateKey(form.sshKey)) {
addLog('error', 'Проверьте формат приватного ключа!');
return;
}
// Логирование ключа (только для отладки!)
console.log('SSH ключ (начало):', form.sshKey.slice(0, 40));
console.log('SSH ключ (конец):', form.sshKey.slice(-40));
console.log('Длина ключа:', form.sshKey.length);
// Логирование отправляемых данных (без самого ключа)
console.log('Данные для агента:', {
...form,
sshKey: form.sshKey ? `[скрыто, длина: ${form.sshKey.length}]` : 'нет ключа'
});
isLoading.value = true;
addLog('info', 'Запуск публикации...');
try {
// Публикация через агента
const result = await webSshService.createTunnel(form);
if (result.success) {
isConnected.value = true;
connectionStatus.value = `Подключено к ${form.domain}`;
addLog('success', 'SSH туннель успешно создан и настроен');
addLog('info', `Ваше приложение доступно по адресу: https://${form.domain}`);
} else {
addLog('error', result.message || 'Ошибка при создании туннеля');
}
} catch (error) {
addLog('error', `Ошибка: ${error.message}`);
} finally {
isLoading.value = false;
}
};
const disconnectTunnel = async () => {
isLoading.value = true;
addLog('info', 'Отключаю SSH туннель...');
try {
const result = await webSshService.disconnectTunnel();
if (result.success) {
isConnected.value = false;
connectionStatus.value = 'Не подключено';
addLog('success', 'SSH туннель отключен');
} else {
addLog('error', result.message || 'Ошибка при отключении туннеля');
}
} catch (error) {
addLog('error', `Ошибка: ${error.message}`);
} finally {
isLoading.value = false;
}
};
const validateForm = () => {
if (!form.domain || !form.email || !form.sshHost || !form.sshUser || !form.sshKey) {
addLog('error', 'Заполните все обязательные поля');
return false;
}
if (!form.email.includes('@')) {
addLog('error', 'Введите корректный email');
return false;
}
if (!form.sshKey.includes('-----BEGIN') || !form.sshKey.includes('-----END')) {
addLog('error', 'SSH ключ должен быть в формате OpenSSH');
return false;
}
return true;
};
const resetForm = () => {
Object.assign(form, {
domain: '',
email: '',
sshHost: '',
sshUser: '',
sshKey: '',
localPort: 5173,
serverPort: 9000,
sshPort: 22
});
logs.value = [];
};
const addLog = (type, message) => {
logs.value.push({
type,
message,
timestamp: new Date()
});
};
const formatTime = (timestamp) => {
return timestamp.toLocaleTimeString();
};
const checkConnectionStatus = async () => {
try {
const status = await webSshService.getStatus();
isConnected.value = status.connected;
connectionStatus.value = status.connected
? `Подключено к ${status.domain}`
: 'Не подключено';
} catch (error) {
console.error('Ошибка проверки статуса:', error);
}
};
const checkAgent = async () => {
const status = await webSshService.checkAgentStatus();
agentAvailable.value = status.running;
};
// Жизненный цикл
onMounted(() => {
checkAgent();
checkConnectionStatus();
// Проверяем статус каждые 30 секунд
const interval = setInterval(checkConnectionStatus, 30000);
onUnmounted(() => {
clearInterval(interval);
});
});
</script>
<style scoped>
.web-ssh-settings {
max-width: 800px;
margin: 0 auto;
}
.settings-header {
margin-bottom: 2rem;
}
.settings-header h2 {
color: var(--color-primary);
margin-bottom: 0.5rem;
}
.settings-header p {
color: var(--color-text-secondary);
font-size: 0.95rem;
}
.connection-status {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--color-background);
border-radius: 8px;
margin-bottom: 2rem;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
transition: background-color 0.3s;
}
.status-indicator.active {
background-color: var(--color-success);
}
.status-indicator.inactive {
background-color: var(--color-text-secondary);
}
.status-text {
font-weight: 500;
flex: 1;
}
.disconnect-btn {
background: var(--color-danger);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.disconnect-btn:hover {
background: var(--color-danger-dark);
}
.tunnel-form {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem;
margin-bottom: 2rem;
}
.form-section {
margin-bottom: 2rem;
}
.form-section h3 {
color: var(--color-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--color-text);
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 0.95rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-primary);
}
.form-group input:disabled,
.form-group textarea:disabled {
background-color: var(--color-background);
cursor: not-allowed;
}
.form-group textarea {
resize: vertical;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
.advanced-section {
border-top: 1px solid var(--color-border);
padding-top: 2rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.publish-btn {
background: var(--color-primary);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: background-color 0.2s;
}
.publish-btn:hover:not(:disabled) {
background: var(--color-primary-dark);
}
.publish-btn:disabled {
background: var(--color-text-secondary);
cursor: not-allowed;
}
.reset-btn {
background: var(--color-background);
color: var(--color-text);
border: 1px solid var(--color-border);
padding: 0.75rem 2rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
.reset-btn:hover:not(:disabled) {
background: var(--color-border);
}
.reset-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.operation-log {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
padding: 2rem;
}
.operation-log h3 {
color: var(--color-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.log-container {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 1rem;
background: var(--color-background);
}
.log-entry {
display: flex;
gap: 1rem;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.log-entry:last-child {
margin-bottom: 0;
}
.log-time {
color: var(--color-text-secondary);
font-family: 'Courier New', monospace;
min-width: 80px;
}
.log-message {
flex: 1;
}
.log-entry.info .log-message {
color: var(--color-text);
}
.log-entry.success .log-message {
color: var(--color-success);
}
.log-entry.error .log-message {
color: var(--color-danger);
}
/* Адаптивный дизайн */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.connection-status {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
}
.agent-instruction-block {
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 8px;
padding: 1.5rem 2rem;
margin-bottom: 2rem;
text-align: left;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.check-btn {
margin-top: 1rem;
background: var(--color-primary);
color: white;
border: none;
padding: 0.5rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
}
pre {
background: #f5f5f5;
border-radius: 4px;
padding: 0.5em 1em;
margin: 0.5em 0;
font-size: 1em;
user-select: all;
white-space: pre-line;
}
.copy-block {
display: flex;
align-items: center;
cursor: pointer;
margin-bottom: 0.5em;
}
.copy-icon {
margin-left: 0.5em;
font-size: 1.2em;
color: #888;
transition: color 0.2s;
display: flex;
align-items: center;
}
.copy-block:hover .copy-icon svg {
stroke: var(--color-primary);
}
.copied-indicator {
color: var(--color-success, #27ae60);
font-weight: 500;
margin-top: 0.5em;
text-align: right;
}
</style>

View File

@@ -21,7 +21,7 @@ const router = useRouter();
const { isAdmin } = useAuthContext();
async function remove() {
await axios.delete(`/api/tables/${$route.params.id}`);
await axios.delete(`/tables/${$route.params.id}`);
router.push({ name: 'tables-list' });
}
function cancel() {

View File

@@ -32,7 +32,7 @@ const description = ref('');
const isRagSourceId = ref(2);
onMounted(async () => {
const { data } = await axios.get(`/api/tables/${$route.params.id}`);
const { data } = await axios.get(`/tables/${$route.params.id}`);
name.value = data.name;
description.value = data.description;
isRagSourceId.value = data.is_rag_source_id || 2;