update
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Box, Container, Grid, Typography, IconButton, Link, Stack, useTheme } from '@mui/material';
|
||||
import { GitHub, YouTube, Telegram, Article } from '@mui/icons-material';
|
||||
import { Box, Container, Grid, IconButton, Link, Stack } from '@mui/material';
|
||||
import { GitHub, YouTube, Telegram } from '@mui/icons-material';
|
||||
|
||||
export default function Footer() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="footer"
|
||||
sx={{
|
||||
py: 3,
|
||||
px: 2,
|
||||
mt: 'auto', // Ключевой стиль для прижатия к низу
|
||||
mt: 'auto',
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.grey[200]
|
||||
@@ -21,18 +18,15 @@ export default function Footer() {
|
||||
<Container maxWidth={false}>
|
||||
<Grid container spacing={4} justifyContent="space-between" alignItems="center">
|
||||
|
||||
{/* Логотип и копирайт */}
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<img src="/img/logo.png" alt="Logo" width={32} height={32} style={{ marginRight: 14 }} />
|
||||
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Ссылка на документацию */}
|
||||
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: { xs: 'left', sm: 'center' } }}>
|
||||
<Link
|
||||
href="https://3dp-manager.com/docs/intro" // Ссылка на ваш репо или доку
|
||||
href="https://3dp-manager.com/docs/intro"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
color="text.primary"
|
||||
@@ -43,7 +37,6 @@ export default function Footer() {
|
||||
</Link>
|
||||
</Grid>
|
||||
|
||||
{/* Социальные иконки */}
|
||||
<Grid size={{ xs: 12, sm: 4 }} sx={{ textAlign: { xs: 'left', sm: 'right' } }}>
|
||||
<Stack direction="row" spacing={1} justifyContent={{ xs: 'flex-start', sm: 'flex-end' }}>
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box, Button, Typography, Paper, Table, TableBody, TableCell,
|
||||
TableHead, TableRow, IconButton, Dialog, DialogTitle,
|
||||
DialogContent, TextField, DialogActions
|
||||
DialogContent, TextField, DialogActions,
|
||||
FormControl,
|
||||
Select,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
type SelectChangeEvent
|
||||
} from '@mui/material';
|
||||
import { Delete, Add, Link as LinkIcon, Refresh, QrCode, Share, OpenInNew, CopyAll, ContentCopy } from '@mui/icons-material';
|
||||
import { Delete, Add, Link as LinkIcon, Refresh, OpenInNew, ContentCopy, Dns, Router } from '@mui/icons-material';
|
||||
import api from '../api';
|
||||
|
||||
interface Subscription {
|
||||
@@ -14,12 +19,20 @@ interface Subscription {
|
||||
inbounds: any[];
|
||||
}
|
||||
|
||||
interface Tunnel {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export default function SubscriptionsPage() {
|
||||
const [subs, setSubs] = useState<Subscription[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [tunnels, setTunnels] = useState<Tunnel[]>([]);
|
||||
|
||||
const [selectedServer, setSelectedServer] = useState<string>('main');
|
||||
|
||||
// Для модалки со ссылками
|
||||
const [linksOpen, setLinksOpen] = useState(false);
|
||||
const [currentLinks, setCurrentLinks] = useState<string[]>([]);
|
||||
|
||||
@@ -28,6 +41,8 @@ export default function SubscriptionsPage() {
|
||||
const loadSubs = async () => {
|
||||
const { data } = await api.get('/subscriptions');
|
||||
setSubs(data);
|
||||
const tunnelsRes = await api.get('/tunnels');
|
||||
setTunnels(tunnelsRes.data);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
@@ -54,16 +69,49 @@ export default function SubscriptionsPage() {
|
||||
setLinksOpen(true);
|
||||
};
|
||||
|
||||
const handleServerChange = (event: SelectChangeEvent) => {
|
||||
setSelectedServer(event.target.value as string);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Typography variant="h4">Подписки</Typography>
|
||||
{tunnels.length > 0 && (
|
||||
<FormControl variant='standard' size="small" sx={{ minWidth: 220, justifyContent: 'center' }}>
|
||||
<Select
|
||||
labelId="server-select-label"
|
||||
value={selectedServer}
|
||||
onChange={handleServerChange}
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
{selectedServer === 'main' ? <Dns fontSize="small"/> : <Router fontSize="small"/>}
|
||||
</InputAdornment>
|
||||
}
|
||||
>
|
||||
<MenuItem value="main">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>Основной сервер</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
{tunnels.map((t) => (
|
||||
<MenuItem key={t.id} value={t.id.toString()}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{t.name}</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
<Box>
|
||||
<Button startIcon={<Refresh />} onClick={loadSubs} sx={{ mr: 1 }}>Обновить</Button>
|
||||
<Button variant="contained" startIcon={<Add />} onClick={() => setOpen(true)}>Создать</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
<Paper>
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -83,14 +131,14 @@ export default function SubscriptionsPage() {
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => navigator.clipboard.writeText(`http://localhost:3000/bus/${sub.uuid}`)}
|
||||
onClick={() => navigator.clipboard.writeText(selectedServer === 'main' ? `http://localhost:3000/bus/${sub.uuid}` : `http://localhost:3000/bus/${sub.uuid}/${selectedServer}`)}
|
||||
title="Копировать ссылку"
|
||||
>
|
||||
<ContentCopy />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => window.open(`http://localhost:3000/bus/${sub.uuid}`, '_blank')}
|
||||
onClick={() => window.open(selectedServer === 'main' ? `http://localhost:3000/bus/${sub.uuid}` : `http://localhost:3000/bus/${sub.uuid}/${selectedServer}`, '_blank')}
|
||||
title="Открыть подписку"
|
||||
>
|
||||
<OpenInNew />
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function TunnelsPage() {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Typography variant="h4">Редирект серверы (Туннели)</Typography>
|
||||
<Typography variant="h4">Relay серверы</Typography>
|
||||
<Button variant="contained" startIcon={<Add />} onClick={() => setOpen(true)}>Добавить</Button>
|
||||
</Box>
|
||||
|
||||
@@ -101,16 +101,18 @@ export default function TunnelsPage() {
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Button
|
||||
startIcon={loadingId === t.id ? <CircularProgress size={20} /> : <Terminal />}
|
||||
disabled={loadingId !== null} // Блокируем всё, пока идет установка
|
||||
onClick={() => handleInstall(t.id)}
|
||||
sx={{ mr: 1 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{loadingId === t.id ? 'Установка...' : 'Установить'}
|
||||
</Button>
|
||||
{!t.isInstalled && (
|
||||
<Button
|
||||
startIcon={loadingId === t.id ? <CircularProgress size={20} /> : <Terminal />}
|
||||
disabled={loadingId !== null}
|
||||
onClick={() => handleInstall(t.id)}
|
||||
sx={{ mr: 1 }}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
{loadingId === t.id ? 'Установка...' : 'Установить'}
|
||||
</Button>
|
||||
)}
|
||||
<IconButton color="inherit" onClick={() => handleDelete(t.id)}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
|
||||
+21
-28
@@ -1,30 +1,27 @@
|
||||
import type { PaletteMode } from '@mui/material';
|
||||
import { amber, deepOrange, grey } from '@mui/material/colors';
|
||||
|
||||
// 1. Определение цветов для Светлой темы
|
||||
const lightPalette = {
|
||||
primary: {
|
||||
main: '#2563eb', // Насыщенный синий (Tailwind Blue 600)
|
||||
main: '#2563eb',
|
||||
light: '#60a5fa',
|
||||
dark: '#1e40af',
|
||||
},
|
||||
secondary: {
|
||||
main: '#7c3aed', // Фиолетовый
|
||||
main: '#7c3aed',
|
||||
},
|
||||
background: {
|
||||
default: '#f3f4f6', // Светло-серый фон (не чисто белый)
|
||||
paper: '#ffffff', // Карточки белые
|
||||
default: '#f3f4f6',
|
||||
paper: '#ffffff',
|
||||
},
|
||||
text: {
|
||||
primary: '#111827', // Почти черный
|
||||
secondary: '#6b7280', // Серый текст
|
||||
primary: '#111827',
|
||||
secondary: '#6b7280',
|
||||
},
|
||||
};
|
||||
|
||||
// 2. Определение цветов для Темной темы
|
||||
const darkPalette = {
|
||||
primary: {
|
||||
main: '#3b82f6', // Чуть светлее синий для контраста на темном
|
||||
main: '#3b82f6',
|
||||
light: '#60a5fa',
|
||||
dark: '#1d4ed8',
|
||||
},
|
||||
@@ -32,16 +29,15 @@ const darkPalette = {
|
||||
main: '#8b5cf6',
|
||||
},
|
||||
background: {
|
||||
default: '#0B0F19', // Глубокий темный (Deep Space), лучше чем #121212
|
||||
paper: '#111827', // Чуть светлее фона (Gray 900)
|
||||
default: '#0B0F19',
|
||||
paper: '#111827',
|
||||
},
|
||||
text: {
|
||||
primary: '#f9fafb', // Почти белый
|
||||
secondary: '#9ca3af', // Светло-серый
|
||||
primary: '#f9fafb',
|
||||
secondary: '#9ca3af',
|
||||
},
|
||||
};
|
||||
|
||||
// 3. Функция генерации настроек
|
||||
export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
palette: {
|
||||
mode,
|
||||
@@ -56,15 +52,14 @@ export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
h5: { fontWeight: 600 },
|
||||
h6: { fontWeight: 600 },
|
||||
button: {
|
||||
textTransform: 'none' as const, // Убираем CAPS LOCK на кнопках
|
||||
textTransform: 'none' as const,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 12, // Скругляем углы у всего (кнопки, карты)
|
||||
borderRadius: 12,
|
||||
},
|
||||
components: {
|
||||
// Кастомизация глобальных стилей (скроллбар)
|
||||
MuiCssBaseline: {
|
||||
styleOverrides: {
|
||||
body: {
|
||||
@@ -87,7 +82,6 @@ export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
// Кастомизация Кнопок
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@@ -104,11 +98,10 @@ export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
// Кастомизация Карточек (Paper)
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none', // Убираем осветление в темной теме (стандарт Material)
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
elevation1: {
|
||||
boxShadow: mode === 'light'
|
||||
@@ -118,7 +111,6 @@ export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
// Кастомизация Инпутов
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@@ -131,12 +123,11 @@ export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
// Кастомизация AppBar (Хедера)
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: mode === 'light' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(17, 24, 39, 0.8)',
|
||||
backdropFilter: 'blur(8px)', // Эффект стекла
|
||||
backdropFilter: 'blur(8px)',
|
||||
borderBottom: `1px solid ${mode === 'light' ? '#e5e7eb' : '#374151'}`,
|
||||
boxShadow: 'none',
|
||||
color: mode === 'light' ? '#111827' : '#f9fafb',
|
||||
@@ -144,10 +135,12 @@ export const getDesignTokens = (mode: PaletteMode) => ({
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
root: {
|
||||
"&:last-child td": {
|
||||
borderBottom: 0,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: {
|
||||
"&:last-child td": {
|
||||
borderBottom: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, Param, HttpException, HttpStatus, Res, Req, Inject } from '@nestjs/common';
|
||||
import { Controller, Get, Param, HttpException, HttpStatus, Res, Req, Inject, Query } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { Response, Request } from 'express';
|
||||
@@ -7,12 +7,15 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import type { Cache } from 'cache-manager';
|
||||
import { Subscription } from '../subscriptions/entities/subscription.entity';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
import { Tunnel } from 'src/tunnels/entities/tunnel.entity';
|
||||
|
||||
@Controller() // Убираем 'client', так как путь зададим явно
|
||||
@Controller()
|
||||
export class ClientController {
|
||||
constructor(
|
||||
@InjectRepository(Subscription)
|
||||
private subRepo: Repository<Subscription>,
|
||||
@InjectRepository(Tunnel)
|
||||
private tunnelRepo: Repository<Tunnel>,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache
|
||||
) { }
|
||||
|
||||
@@ -130,4 +133,154 @@ export class ClientController {
|
||||
res.send(html);
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('bus/:uuid/:tunnelId')
|
||||
async getRelaySubscription(
|
||||
@Param('uuid') uuid: string,
|
||||
@Param('tunnelId') tunnelId: string,
|
||||
@Query('format') format: string,
|
||||
@Req() req: Request,
|
||||
@Res() res: Response
|
||||
) {
|
||||
const tunnel = await this.tunnelRepo.findOne({ where: { id: +tunnelId } });
|
||||
if (!tunnel) {
|
||||
return res.status(HttpStatus.NOT_FOUND).send('Relay server not found');
|
||||
}
|
||||
|
||||
const relayHost = tunnel.domain || tunnel.ip;
|
||||
|
||||
const sub = await this.subRepo.findOne({
|
||||
where: { uuid },
|
||||
relations: ['inbounds']
|
||||
});
|
||||
|
||||
if (!sub || !sub.isEnabled) {
|
||||
throw new HttpException('Subscription not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
let links = sub.inbounds
|
||||
?.map(i => i.link)
|
||||
.filter(l => l && l.length > 0) || [];
|
||||
|
||||
links = links.map(link => this.patchLink(link, relayHost));
|
||||
|
||||
const plainTextList = links.join('\n');
|
||||
const base64Config = Buffer.from(plainTextList).toString('base64');
|
||||
|
||||
const userAgent = req.headers['user-agent'] || '';
|
||||
const isBrowser = /Mozilla|Chrome|Safari|Firefox|Edge/.test(userAgent);
|
||||
|
||||
if (!isBrowser) {
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(base64Config);
|
||||
} else {
|
||||
|
||||
const currentUrl = `${req.protocol}://${req.get('host')}/bus/${uuid}/${tunnelId}`;
|
||||
|
||||
const cacheKey = `qr_${uuid}_${relayHost || 'direct'}`;
|
||||
|
||||
let qrDataUrl = await this.cacheManager.get<string>(cacheKey);
|
||||
|
||||
if (!qrDataUrl) {
|
||||
qrDataUrl = await QRCode.toDataURL(currentUrl, { width: 300, margin: 2 });
|
||||
|
||||
await this.cacheManager.set(cacheKey, qrDataUrl, 86400000);
|
||||
} else {
|
||||
console.log(`Взяли QR из кэша для ${uuid}`);
|
||||
}
|
||||
|
||||
// HTML шаблон
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${sub.name} | 3DP-MANAGER</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f4f6f8; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||
.card { background: white; padding: 2rem; border-radius: 16px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; max-width: 400px; width: 90%; }
|
||||
h2 { margin-top: 0; color: #333; }
|
||||
.qr-box { background: #fff; padding: 10px; border: 1px solid #eee; border-radius: 8px; display: inline-block; margin: 20px 0; }
|
||||
.link-box { background: #f5f5f5; padding: 10px; border-radius: 6px; font-family: monospace; word-break: break-all; font-size: 12px; color: #666; margin-bottom: 20px; border: 1px solid #e0e0e0; }
|
||||
button { background-color: #1976d2; color: white; border: none; padding: 12px 24px; border-radius: 8px; font-size: 16px; cursor: pointer; transition: background 0.2s; width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
button:hover { background-color: #1565c0; }
|
||||
button:active { transform: scale(0.98); }
|
||||
.note { margin-top: 20px; font-size: 12px; color: #999; }
|
||||
|
||||
#subscription-links { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>Ваша подписка</h2>
|
||||
<p style="color: #666;">Отсканируйте QR-код в приложении Happ, v2RayTun или Streisand</p>
|
||||
|
||||
<div class="qr-box">
|
||||
<img src="${qrDataUrl}" alt="QR Code" />
|
||||
</div>
|
||||
|
||||
<div class="link-box" id="link-text">${currentUrl}</div>
|
||||
|
||||
<button onclick="copyLink()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z"/></svg>
|
||||
Копировать ссылку
|
||||
</button>
|
||||
|
||||
<div class="note">Для автоматического обновления конфигов используйте эту ссылку</div>
|
||||
|
||||
</div>
|
||||
<textarea id="subscription-links">${base64Config}</textarea>
|
||||
|
||||
<script>
|
||||
function copyLink() {
|
||||
const link = document.getElementById('link-text').innerText;
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
const btn = document.querySelector('button');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = 'Скопировано!';
|
||||
btn.style.backgroundColor = '#2e7d32';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalText;
|
||||
btn.style.backgroundColor = '#1976d2';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
}
|
||||
}
|
||||
|
||||
private patchLink(link: string, newHost: string): string {
|
||||
if (link.startsWith('vmess://')) {
|
||||
try {
|
||||
const base64Part = link.substring(8);
|
||||
const jsonStr = Buffer.from(base64Part, 'base64').toString('utf-8');
|
||||
const config = JSON.parse(jsonStr);
|
||||
|
||||
config.add = newHost;
|
||||
|
||||
const newJsonStr = JSON.stringify(config);
|
||||
const newBase64 = Buffer.from(newJsonStr).toString('base64');
|
||||
return `vmess://${newBase64}`;
|
||||
} catch (e) {
|
||||
return link;
|
||||
}
|
||||
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
||||
return link.replace(/@.*?:/, `@${newHost}:`);
|
||||
} else if (link.startsWith('ss://')) {
|
||||
if (link.includes('@')) {
|
||||
return link.replace(/@.*?:/, `@${newHost}:`);
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ClientController } from './client.controller';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { Subscription } from '../subscriptions/entities/subscription.entity';
|
||||
import { Tunnel } from 'src/tunnels/entities/tunnel.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Subscription]), CacheModule.register()],
|
||||
imports: [TypeOrmModule.forFeature([Subscription, Tunnel]), CacheModule.register()],
|
||||
controllers: [ClientController],
|
||||
})
|
||||
export class ClientModule {}
|
||||
+4
-1
@@ -11,7 +11,10 @@ async function bootstrap() {
|
||||
|
||||
app.enableCors();
|
||||
app.setGlobalPrefix('api', {
|
||||
exclude: [{ path: 'bus/:uuid', method: RequestMethod.GET }]
|
||||
exclude: [
|
||||
{ path: 'bus/:uuid', method: RequestMethod.GET },
|
||||
{ path: 'bus/:uuid/:tunnelId', method: RequestMethod.GET },
|
||||
]
|
||||
});
|
||||
|
||||
await app.listen(3000);
|
||||
|
||||
@@ -53,7 +53,7 @@ export class TunnelsService {
|
||||
|
||||
// 3. Формируем команду
|
||||
// export ORIGIN_IP="1.2.3.4" && bash <(curl ...)
|
||||
const command = `export ORIGIN_IP="${mainServerIp}" && bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/forwarding_install.sh)`;
|
||||
const command = `export ORIGIN_IP="${mainServerIp}" && bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/dp-gui/forwarding_install.sh)`;
|
||||
|
||||
try {
|
||||
// 4. Выполняем через SSH
|
||||
|
||||
Reference in New Issue
Block a user