feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI

This commit is contained in:
2026-09-28 10:39:19 +05:00
parent edcd30c829
commit be68f5e130
204 changed files with 24616 additions and 39 deletions
+47
View File
@@ -0,0 +1,47 @@
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: xflow
POSTGRES_PASSWORD: xflow_pass
POSTGRES_DB: xflow
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U xflow -d xflow" ]
interval: 5s
timeout: 5s
retries: 5
xflow-dev:
image: node:20-alpine
working_dir: /app
environment:
DATABASE_URL: postgresql://xflow:xflow_pass@postgres:5432/xflow
NODE_ENV: development
NUXT_PUBLIC_FIREBASE_API_KEY: ""
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ""
NUXT_PUBLIC_FIREBASE_PROJECT_ID: ""
ports:
- "3000:3000"
volumes:
- .:/app
- node_modules:/app/node_modules
command: sh -c "npm install && npx prisma generate && npx prisma migrate deploy --config prisma.config.ts && npm run dev"
depends_on:
postgres:
condition: service_healthy
networks:
- xflow-dev
volumes:
postgres_data:
node_modules:
networks:
xflow-dev:
driver: bridge
+39
View File
@@ -0,0 +1,39 @@
# --- Database Settings ---
POSTGRES_USER=admin
POSTGRES_PASSWORD=your_secure_password
DB_NAME_GITEA=gitea_db
DB_NAME_NPB=npb_app_db
DB_NAME_XFLOW=xflow_crm_db
# --- External Ports ---
DB_EXTERNAL_PORT=5432
ADMINER_PORT=8080
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001
GITEA_EXTERNAL_PORT=3001
XFLOW_PORT=3002
NBP_PORT=3000
# --- MinIO Settings ---
MINIO_ROOT_USER=admin
MINIO_ROOT_PASSWORD=your_minio_password
# --- Gitea Settings ---
GITEA_RUNNER_TOKEN=your_runner_token
GITEA_RUNNER_NAME=avtex-runner
GITEA_TOKEN=your_personal_access_token
GITEA_ORG=AVTEX
GITEA_MINDMAP_REPO=MMAVTEX
GITEA_PROJECT_REPO=XFlow
# --- Firebase Settings ---
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_APP_ID=
FIREBASE_MESSAGING_SENDER_ID=
# --- AI & External APIs ---
GOOGLE_API_KEY=
REPLICATE_API_KEY=
+25
View File
@@ -0,0 +1,25 @@
FROM node:20-alpine
# Install git for repository operations
RUN apk add --no-cache git
# Create app directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy app source
COPY src/ ./src/
# Create directories
RUN mkdir -p /app/mindmaps /app/logs
# Expose port
EXPOSE 8080
# Start the server
CMD ["npm", "start"]
+34
View File
@@ -0,0 +1,34 @@
{
"name": "xflow-api-server",
"version": "1.0.0",
"description": "API server for XFlow CRM",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"axios": "^1.6.2",
"simple-git": "^3.21.0",
"fs-extra": "^11.2.0",
"uuid": "^9.0.1",
"dotenv": "^16.3.1",
"express-rate-limit": "^7.1.5",
"multer": "^1.4.5-lts.1",
"yaml": "^2.3.4"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+58
View File
@@ -0,0 +1,58 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const authRoutes = require('./routes/auth');
const { errorHandler } = require('./middleware/errorHandler');
const app = express();
const PORT = process.env.PORT || 8080;
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// Logging
app.use(morgan('combined'));
// Health check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: '1.0.0'
});
});
// API Routes
app.use('/api/auth', authRoutes);
// Error handling
app.use(errorHandler);
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 XFlow API Server running on port ${PORT}`);
});
@@ -0,0 +1,34 @@
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key';
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
const generateToken = (user) => {
return jwt.sign(
{
id: user.id,
email: user.email,
username: user.username
},
JWT_SECRET,
{ expiresIn: '7d' }
);
};
module.exports = { authenticateToken, generateToken };
@@ -0,0 +1,33 @@
const errorHandler = (err, req, res, next) => {
console.error('Error:', err);
// Default error
let statusCode = err.statusCode || 500;
let message = err.message || 'Internal Server Error';
// Handle specific error types
if (err.name === 'ValidationError') {
statusCode = 400;
message = 'Validation Error: ' + message;
} else if (err.name === 'UnauthorizedError') {
statusCode = 401;
message = 'Unauthorized';
} else if (err.name === 'ForbiddenError') {
statusCode = 403;
message = 'Forbidden';
} else if (err.name === 'NotFoundError') {
statusCode = 404;
message = 'Resource not found';
}
res.status(statusCode).json({
error: {
message,
status: statusCode,
timestamp: new Date().toISOString(),
path: req.path
}
});
};
module.exports = { errorHandler };
@@ -0,0 +1,97 @@
const express = require('express');
const router = express.Router();
const { generateToken } = require('../middleware/auth');
// Mock user storage (в продакшене использовать базу данных)
const users = new Map();
// Register
router.post('/register', async (req, res) => {
try {
const { email, password, username } = req.body;
if (!email || !password || !username) {
return res.status(400).json({ error: 'Email, password and username are required' });
}
if (users.has(email)) {
return res.status(409).json({ error: 'User already exists' });
}
const userId = Date.now().toString();
const user = {
id: userId,
email,
username,
password, // В продакшене хешировать!
createdAt: new Date().toISOString()
};
users.set(email, user);
const token = generateToken(user);
res.status(201).json({
message: 'User registered successfully',
token,
user: {
id: user.id,
email: user.email,
username: user.username
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
const user = users.get(email);
if (!user || user.password !== password) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user);
res.json({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
username: user.username
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Validate token
router.get('/validate', async (req, res) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ valid: false, error: 'No token provided' });
}
try {
const jwt = require('jsonwebtoken');
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-super-secret-jwt-key');
res.json({ valid: true, user: decoded });
} catch (error) {
res.status(403).json({ valid: false, error: 'Invalid token' });
}
});
module.exports = router;
@@ -0,0 +1,169 @@
# XFlow Server + Consyscab Integration
# Объединённая конфигурация для совместного развёртывания
services:
# ==========================================
# 1. Единая база данных PostgreSQL
# ==========================================
consyscab_db:
image: postgres:16-alpine
container_name: consyscab_db
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=${DB_PASSWORD:-password}
- POSTGRES_DB=symfony_db
volumes:
- pg_data:/var/lib/postgresql/data
- ./docker/postgres/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
networks:
- consyscab-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
# ==========================================
# 2. PHP 8.4 (Symfony + Moodle)
# ==========================================
consyscab_php:
build:
context: ./docker/php
container_name: consyscab_php
volumes:
- ./backend:/var/www/symfony
- ./moodle:/var/www/moodle
- ./moodledata:/var/www/moodledata
- ./docker/php/custom.ini:/usr/local/etc/php/conf.d/custom.ini:ro
- ~/.gitconfig:/var/www/.gitconfig:ro
depends_on:
consyscab_db:
condition: service_healthy
consyscab_memcached:
condition: service_started
networks:
- consyscab-network
# ==========================================
# 3. Nginx (Consyscab + XFlow)
# ==========================================
consyscab_nginx:
image: nginx:alpine
container_name: consyscab_nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./backend:/var/www/symfony
- ./moodle:/var/www/moodle
- ./docker/nginx/conf.d:/etc/nginx/conf.d
# XFlow: дополнительная конфигурация
- ./xflow/nginx:/etc/nginx/conf.d/xflow:ro
- ./xflow/html:/usr/share/nginx/html/xflow:ro
depends_on:
- consyscab_php
- xflow_gitea
networks:
- consyscab-network
# ==========================================
# 4. Adminer
# ==========================================
consyscab_adminer:
image: adminer
container_name: consyscab_adminer
depends_on:
- consyscab_db
networks:
- consyscab-network
# ==========================================
# 5. Memcached
# ==========================================
consyscab_memcached:
image: memcached:alpine
container_name: consyscab_memcached
networks:
- consyscab-network
# ==========================================
# 6. Node.js для Quasar
# ==========================================
consyscab_front:
image: node:22-alpine
container_name: consyscab_front
user: "1000:1000"
ports:
- "9000:9000"
- "9005:9005"
working_dir: /app
volumes:
- ./frontend:/app
- ~/.gitconfig:/var/www/.gitconfig:ro
environment:
- NODE_ENV=development
tty: true
stdin_open: true
networks:
- consyscab-network
# ==========================================
# 7. Mercure Hub
# ==========================================
consyscab_mercure:
image: dunglas/mercure
container_name: consyscab_mercure
environment:
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET:-secret}
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET:-secret}
ALLOW_ANONYMOUS: 1
networks:
- consyscab-network
# ==========================================
# 8. XFlow Gitea (Git сервер)
# ==========================================
xflow_gitea:
image: gitea/gitea:latest
container_name: xflow_gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=consyscab_db:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=user
- GITEA__database__PASSWD=${DB_PASSWORD:-password}
- GITEA__server__DOMAIN=${GITEA_DOMAIN:-git.localhost}
- GITEA__server__ROOT_URL=http://${GITEA_DOMAIN:-git.localhost}
- GITEA__server__SSH_DOMAIN=${GITEA_DOMAIN:-git.localhost}
- GITEA__server__SSH_PORT=2221
- GITEA__server__SSH_LISTEN_PORT=22
- GITEA__security__INSTALL_LOCK=true
- GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY}
- GITEA__service__DISABLE_REGISTRATION=false
- GITEA__service__REQUIRE_SIGNIN_VIEW=false
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
consyscab_db:
condition: service_healthy
networks:
- consyscab-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
consyscab-network:
driver: bridge
volumes:
pg_data:
gitea_data:
@@ -0,0 +1,31 @@
# Docker Compose Override for Development/Local Testing
# Usage: docker compose -f docker-compose.yml -f docker-compose.override.yml up
services:
# Use standard ports for local development
gitea:
ports:
- "3000:3000"
- "222:22"
environment:
- GITEA__server__ROOT_URL=http://localhost:3000
- GITEA__server__SSH_PORT=222
postgres:
ports:
- "5432:5432"
xflow-api:
ports:
- "8080:8080"
environment:
- NODE_ENV=development
nginx:
ports:
- "80:80"
- "443:443"
webdav:
ports:
- "8081:80"
@@ -0,0 +1,198 @@
version: '3.8'
# Production Docker Compose Configuration
# Usage: docker compose -f docker-compose.prod.yml up -d
services:
# ==========================================
# Gitea - Production Configuration
# ==========================================
gitea:
image: gitea/gitea:latest-rootless
container_name: xflow-gitea
restart: unless-stopped
networks:
- xflow-network
volumes:
- gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=postgres:5432
- GITEA__database__NAME=gitea
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
- GITEA__server__DOMAIN=${GITEA_DOMAIN}
- GITEA__server__ROOT_URL=https://${GITEA_DOMAIN}
- GITEA__server__SSH_DOMAIN=${GITEA_DOMAIN}
- GITEA__server__SSH_PORT=222
- GITEA__server__SSH_LISTEN_PORT=22
- GITEA__security__INSTALL_LOCK=true
- GITEA__security__SECRET_KEY=${GITEA_SECRET_KEY}
- GITEA__service__DISABLE_REGISTRATION=false
- GITEA__service__REQUIRE_SIGNIN_VIEW=false
- GITEA__log__MODE=console
- GITEA__log__LEVEL=Info
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
# ==========================================
# PostgreSQL - Production
# ==========================================
postgres:
image: postgres:16-alpine
container_name: xflow-postgres
restart: unless-stopped
networks:
- xflow-network
volumes:
- postgres-data:/var/lib/postgresql/data
- ./backups:/backups
environment:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=gitea
- PGDATA=/var/lib/postgresql/data/pgdata
healthcheck:
test: ["CMD-SHELL", "pg_isready -U gitea"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
# ==========================================
# Nginx - Production with SSL
# ==========================================
nginx:
image: nginx:alpine
container_name: xflow-nginx
restart: unless-stopped
networks:
- xflow-network
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/html:/usr/share/nginx/html:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- certbot-data:/etc/letsencrypt:ro
- certbot-www:/var/www/certbot:ro
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
depends_on:
- gitea
# ==========================================
# Certbot - SSL Certificates
# ==========================================
certbot:
image: certbot/certbot
container_name: xflow-certbot
restart: "no"
networks:
- xflow-network
volumes:
- certbot-data:/etc/letsencrypt
- certbot-www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
profiles:
- ssl
# ==========================================
# Watchtower - Auto-update containers
# ==========================================
watchtower:
image: containrrr/watchtower
container_name: xflow-watchtower
restart: unless-stopped
networks:
- xflow-network
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_POLL_INTERVAL=3600
- WATCHTOWER_INCLUDE_STOPPED=true
- WATCHTOWER_REVIVE_STOPPED=false
- WATCHTOWER_ROLLING_RESTART=true
- WATCHTOWER_LABEL_ENABLE=true
command: --interval 3600 --cleanup
profiles:
- auto-update
# ==========================================
# Redis - Caching (optional)
# ==========================================
redis:
image: redis:7-alpine
container_name: xflow-redis
restart: unless-stopped
networks:
- xflow-network
volumes:
- redis-data:/data
command: redis-server --appendonly yes --maxmemory 64mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
profiles:
- cache
networks:
xflow-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
gitea-data:
driver: local
postgres-data:
driver: local
api-logs:
driver: local
certbot-data:
driver: local
certbot-www:
driver: local
redis-data:
driver: local
+40
View File
@@ -0,0 +1,40 @@
version: '3.8'
# XFlow CRM Service Configuration
# Этот файл описывает только само приложение.
# База данных, Gitea и хранилища являются внешними сервисами.
services:
xflow_crm:
image: node:22-alpine
container_name: xflow_crm
restart: always
working_dir: /app
command: node .output/server/index.mjs
ports:
- "${XFLOW_PORT:-3002}:3000"
environment:
NODE_ENV: production
PORT: 3000
GITEA_URL: http://xflow_gitea:3000
GITEA_TOKEN: ${GITEA_TOKEN}
GITEA_ORG: ${GITEA_ORG:-AVTEX}
GITEA_MINDMAP_REPO: ${GITEA_MINDMAP_REPO:-MMAVTEX}
GITEA_PROJECT_REPO: ${GITEA_PROJECT_REPO:-XFlow}
DATABASE_URL: postgresql://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD}@xflow_db:5432/${DB_NAME_XFLOW:-xflow_crm_db}
NUXT_FIREBASE_API_KEY: ${FIREBASE_API_KEY}
NUXT_PUBLIC_FIREBASE_API_KEY: ${FIREBASE_API_KEY}
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${FIREBASE_AUTH_DOMAIN}
NUXT_PUBLIC_FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID}
NUXT_PUBLIC_FIREBASE_APP_ID: ${FIREBASE_APP_ID}
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${FIREBASE_MESSAGING_SENDER_ID}
volumes:
- .:/app # Монтируем текущую папку проекта
networks:
- xflow_infra
networks:
xflow_infra:
external: true
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
# Подключаемся к postgres под суперпользователем и создаем базы
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "postgres" <<-EOSQL
CREATE DATABASE "$DB_NAME_GITEA";
CREATE DATABASE "$DB_NAME_NPB";
CREATE DATABASE "$DB_NAME_XFLOW";
EOSQL
+29
View File
@@ -0,0 +1,29 @@
module.exports = {
apps: [{
name: 'xflow-api',
script: './api/src/index.js',
instances: 1,
exec_mode: 'fork',
env: {
NODE_ENV: 'production',
PORT: 3001
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
max_memory_restart: '256M',
restart_delay: 3000,
max_restarts: 5,
min_uptime: '10s',
watch: false,
ignore_watch: ['node_modules', 'logs', 'data'],
merge_logs: true,
log_type: 'json',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
// Auto-restart on failure
autorestart: true,
// Don't restart if crashing too fast
exp_backoff_restart_delay: 100
}]
};
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XFlow Server</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
font-size: 3rem;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.services {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
max-width: 800px;
margin: 0 auto;
}
.service-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s, box-shadow 0.3s;
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.service-card h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
}
.service-card p {
font-size: 0.95rem;
margin-bottom: 1rem;
}
.service-card a {
display: inline-block;
padding: 0.5rem 1.5rem;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 25px;
font-weight: 600;
transition: all 0.3s;
}
.service-card a:hover {
background: #f0f0f0;
transform: scale(1.05);
}
.status {
margin-top: 2rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
display: inline-block;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
background: #4ade80;
border-radius: 50%;
margin-right: 0.5rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 XFlow Server</h1>
<p>XFlow CRM Server</p>
<div class="services">
<div class="service-card">
<h3>📚 Git Repository</h3>
<p>Self-hosted Git server</p>
<a href="/git/" target="_blank">Open Gitea</a>
</div>
<div class="service-card">
<h3>🔌 API</h3>
<p>REST API for CRM operations</p>
<a href="/api/health" target="_blank">Check API</a>
</div>
<div class="service-card">
<h3>📱 WebDAV</h3>
<p>Direct file access</p>
<a href="http://a-crm.controlsystem.kz:8084" target="_blank">Open WebDAV</a>
</div>
</div>
<div class="status">
<span class="status-indicator"></span>
<span>Server is running</span>
</div>
</div>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
# Resolver for Docker DNS
resolver 127.0.0.11 valid=30s;
# Main server - serves static files and proxies to services
server {
listen 80;
server_name _;
# For Let's Encrypt challenges
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# API proxy
location /api/ {
set $api_backend xflow-api:8080;
proxy_pass http://$api_backend;
proxy_http_version 1.1;
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;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Gitea proxy
location /git/ {
set $gitea_backend gitea:3000;
proxy_pass http://$gitea_backend/;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /git;
}
# Default - static files
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
# HTTPS server (uncomment when SSL certificates are ready)
# server {
# listen 443 ssl http2;
# server_name your-domain.com;
#
# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
#
# location / {
# root /usr/share/nginx/html;
# try_files $uri $uri/ /index.html;
# }
#
# location /api/ {
# proxy_pass http://xflow_api/;
# proxy_http_version 1.1;
# 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;
# }
#
# location /git/ {
# proxy_pass http://gitea/;
# proxy_http_version 1.1;
# 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;
# }
# }
}
+38
View File
@@ -0,0 +1,38 @@
{
"name": "xflow-api-server",
"version": "1.0.0",
"description": "API server for XFlow CRM - FastPanel compatible",
"main": "api/src/index.js",
"scripts": {
"start": "node api/src/index.js",
"dev": "nodemon api/src/index.js",
"pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop ecosystem.config.js",
"pm2:restart": "pm2 restart ecosystem.config.js",
"pm2:logs": "pm2 logs xflow-api",
"install:deps": "npm ci --production"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"axios": "^1.6.2",
"simple-git": "^3.21.0",
"fs-extra": "^11.2.0",
"uuid": "^9.0.1",
"dotenv": "^16.3.1",
"express-rate-limit": "^7.1.5",
"multer": "^1.4.5-lts.1",
"yaml": "^2.3.4",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"engines": {
"node": ">=18.0.0"
}
}
+256
View File
@@ -0,0 +1,256 @@
#!/bin/bash
# ==========================================
# XFlow Server Deployment Script
# ==========================================
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
COMPOSE_FILE="docker-compose.prod.yml"
BACKUP_DIR="./backups"
LOG_FILE="./logs/deploy-$(date +%Y%m%d-%H%M%S).log"
# ==========================================
# Helper Functions
# ==========================================
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $1" | tee -a "$LOG_FILE"
}
warning() {
echo -e "${YELLOW}[WARNING]${NC} $1" | tee -a "$LOG_FILE"
}
# ==========================================
# Pre-deployment Checks
# ==========================================
check_prerequisites() {
log "Checking prerequisites..."
# Check Docker
if ! command -v docker &> /dev/null; then
error "Docker is not installed"
exit 1
fi
# Check Docker Compose
if ! docker compose version &> /dev/null; then
error "Docker Compose is not installed"
exit 1
fi
# Check .env file
if [ ! -f .env ]; then
error ".env file not found. Please create it from .env.example"
exit 1
fi
# Check compose file
if [ ! -f "$COMPOSE_FILE" ]; then
error "Compose file $COMPOSE_FILE not found"
exit 1
fi
success "Prerequisites check passed"
}
# ==========================================
# Backup
# ==========================================
create_backup() {
log "Creating backup..."
mkdir -p "$BACKUP_DIR"
# Backup database
if docker compose ps postgres | grep -q "running"; then
log "Backing up PostgreSQL database..."
docker compose exec -T postgres pg_dump -U gitea gitea > "$BACKUP_DIR/db-$(date +%Y%m%d-%H%M%S).sql" 2>/dev/null || warning "Database backup failed"
fi
success "Backup completed"
}
# ==========================================
# Deployment
# ==========================================
deploy() {
log "Starting deployment..."
# Pull latest images
log "Pulling latest images..."
docker compose -f "$COMPOSE_FILE" pull
# Deploy with zero downtime
log "Deploying services..."
docker compose -f "$COMPOSE_FILE" up -d --remove-orphans
success "Deployment completed"
}
# ==========================================
# Health Checks
# ==========================================
health_check() {
log "Running health checks..."
local max_attempts=30
local attempt=1
# Wait for services to start
sleep 10
# Check API
log "Checking API health..."
while [ $attempt -le $max_attempts ]; do
if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
success "API is healthy"
break
fi
if [ $attempt -eq $max_attempts ]; then
error "API health check failed after $max_attempts attempts"
show_logs
exit 1
fi
log "Attempt $attempt/$max_attempts: API not ready yet..."
sleep 5
((attempt++))
done
# Check Gitea
log "Checking Gitea health..."
attempt=1
while [ $attempt -le $max_attempts ]; do
if curl -sf http://localhost:3000/api/healthz > /dev/null 2>&1; then
success "Gitea is healthy"
break
fi
if [ $attempt -eq $max_attempts ]; then
warning "Gitea health check failed (may still be initializing)"
break
fi
log "Attempt $attempt/$max_attempts: Gitea not ready yet..."
sleep 5
((attempt++))
done
success "All health checks passed"
}
# ==========================================
# Cleanup
# ==========================================
cleanup() {
log "Cleaning up..."
# Remove old backups (keep last 7)
if [ -d "$BACKUP_DIR" ]; then
ls -t "$BACKUP_DIR"/*.sql 2>/dev/null | tail -n +8 | xargs -r rm
ls -t "$BACKUP_DIR"/*.tar.gz 2>/dev/null | tail -n +8 | xargs -r rm
fi
# Clean Docker
docker system prune -f
docker volume prune -f
success "Cleanup completed"
}
# ==========================================
# Rollback
# ==========================================
rollback() {
error "Deployment failed! Rolling back..."
# Get previous image
local previous_image=$(docker images ghcr.io/*/xflow-api --format "{{.Repository}}:{{.Tag}}" | grep -v latest | head -1)
if [ -n "$previous_image" ]; then
log "Reverting to: $previous_image"
docker compose -f "$COMPOSE_FILE" stop api
docker compose -f "$COMPOSE_FILE" rm -f api
docker run -d --name xflow-api-rollback \
--network xflow-network \
-e NODE_ENV=production \
-e PORT=8080 \
-p 8080:8080 \
"$previous_image"
success "Rollback completed"
else
error "No previous image found for rollback"
fi
}
# ==========================================
# Show Logs
# ==========================================
show_logs() {
log "Showing recent logs..."
docker compose -f "$COMPOSE_FILE" logs --tail=50
}
# ==========================================
# Main
# ==========================================
main() {
log "======================================"
log "XFlow Server Deployment"
log "======================================"
# Create logs directory
mkdir -p logs
# Run deployment steps
check_prerequisites
create_backup
# Deploy with error handling
if deploy; then
if health_check; then
cleanup
success "======================================"
success "Deployment completed successfully!"
success "======================================"
log "API: http://localhost:8080"
log "Gitea: http://localhost:3000"
else
rollback
exit 1
fi
else
rollback
exit 1
fi
}
# Run main function
main "$@"
+281
View File
@@ -0,0 +1,281 @@
#!/bin/bash
# ==========================================
# VPS Setup Script for XFlow Server
# Run this on your fresh VPS
# ==========================================
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[SETUP]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# ==========================================
# Configuration
# ==========================================
DEPLOY_DIR="/opt/xflow-server"
REPO_URL="" # Will be set interactively
DOMAIN="" # Will be set interactively
# ==========================================
# System Update
# ==========================================
update_system() {
log "Updating system packages..."
apt update && apt upgrade -y
apt install -y curl wget git nano htop ufw fail2ban
success "System updated"
}
# ==========================================
# Install Docker
# ==========================================
install_docker() {
log "Installing Docker..."
if command -v docker &> /dev/null; then
warning "Docker already installed, skipping"
return
fi
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
rm get-docker.sh
usermod -aG docker $USER
success "Docker installed"
}
# ==========================================
# Setup Firewall
# ==========================================
setup_firewall() {
log "Configuring firewall..."
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 3000/tcp # Gitea
ufw allow 8080/tcp # API
ufw allow 222/tcp # Gitea SSH
ufw --force enable
success "Firewall configured"
}
# ==========================================
# Setup Fail2Ban
# ==========================================
setup_fail2ban() {
log "Configuring Fail2Ban..."
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
EOF
systemctl restart fail2ban
success "Fail2Ban configured"
}
# ==========================================
# Setup Deployment Directory
# ==========================================
setup_deploy_dir() {
log "Setting up deployment directory..."
mkdir -p "$DEPLOY_DIR"
cd "$DEPLOY_DIR"
# Clone repository
if [ -n "$REPO_URL" ]; then
git clone "$REPO_URL" .
fi
# Create necessary directories
mkdir -p backups logs
success "Deployment directory ready"
}
# ==========================================
# Setup SSL with Let's Encrypt
# ==========================================
setup_ssl() {
log "Setting up SSL certificates..."
if [ -z "$DOMAIN" ]; then
warning "No domain specified, skipping SSL setup"
return
fi
apt install -y certbot
# Obtain certificates
certbot certonly --standalone -d "$DOMAIN" --agree-tos --non-interactive --email admin@$DOMAIN || {
warning "SSL setup failed, continuing without SSL"
return
}
# Auto-renewal cron job
(crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet") | crontab -
success "SSL certificates configured"
}
# ==========================================
# Create Environment File
# ==========================================
setup_env() {
log "Creating environment configuration..."
cd "$DEPLOY_DIR/server"
# Generate secrets
JWT_SECRET=$(openssl rand -base64 32)
POSTGRES_PASSWORD=$(openssl rand -base64 24)
GITEA_SECRET_KEY=$(openssl rand -base64 32)
WEBDAV_PASSWORD=$(openssl rand -base64 16)
cat > .env << EOF
# Domain configuration
GITEA_DOMAIN=${DOMAIN:-localhost}
# Gitea Configuration
GITEA_TOKEN=
GITEA_SECRET_KEY=$GITEA_SECRET_KEY
# Security
JWT_SECRET=$JWT_SECRET
# WebDAV
WEBDAV_USER=admin
WEBDAV_PASSWORD=$WEBDAV_PASSWORD
# Database
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
# API Configuration
API_PORT=8080
EOF
success "Environment file created"
warning "Please edit .env file and add your GITEA_TOKEN after first Gitea setup"
}
# ==========================================
# Create Systemd Service
# ==========================================
setup_systemd() {
log "Creating systemd service..."
cat > /etc/systemd/system/xflow-server.service << EOF
[Unit]
Description=XFlow Server
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=$DEPLOY_DIR/server
ExecStart=/usr/bin/docker compose -f docker-compose.prod.yml up -d
ExecStop=/usr/bin/docker compose -f docker-compose.prod.yml down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable xflow-server.service
success "Systemd service created"
}
# ==========================================
# Interactive Setup
# ==========================================
interactive_setup() {
echo ""
echo "======================================"
echo "XFlow Server VPS Setup"
echo "======================================"
echo ""
read -p "Enter your GitHub repository URL (or leave empty to skip clone): " REPO_URL
read -p "Enter your domain name (or leave empty for local setup): " DOMAIN
echo ""
log "Starting setup with:"
log " Repository: ${REPO_URL:-None}"
log " Domain: ${DOMAIN:-localhost}"
echo ""
read -p "Continue? (y/n): " confirm
if [ "$confirm" != "y" ]; then
error "Setup cancelled"
exit 1
fi
}
# ==========================================
# Main
# ==========================================
main() {
interactive_setup
log "Starting VPS setup..."
update_system
install_docker
setup_firewall
setup_fail2ban
setup_deploy_dir
setup_ssl
setup_env
setup_systemd
echo ""
success "======================================"
success "VPS Setup Complete!"
success "======================================"
echo ""
log "Next steps:"
echo " 1. Edit $DEPLOY_DIR/server/.env with your settings"
echo " 2. Run: cd $DEPLOY_DIR/server && docker compose -f docker-compose.prod.yml up -d"
echo " 3. Open Gitea at http://${DOMAIN:-your-server-ip}:3000"
echo " 4. Create admin account and API token"
echo " 5. Add GITEA_TOKEN to .env file"
echo " 6. Restart: systemctl restart xflow-server"
echo ""
log "Useful commands:"
echo " - View logs: docker compose -f docker-compose.prod.yml logs -f"
echo " - Restart: systemctl restart xflow-server"
echo " - Update: cd $DEPLOY_DIR && git pull && systemctl restart xflow-server"
echo ""
}
main "$@"
+123
View File
@@ -0,0 +1,123 @@
# XFlow Server Nginx Configuration
# Добавьте этот файл в ./docker/nginx/conf.d/ вашего друга
# Resolver для Docker DNS
resolver 127.0.0.11 valid=30s;
# ==========================================
# Gitea - Git сервер
# ==========================================
server {
listen 80;
server_name git.* ~^git\..+$;
client_max_body_size 512M;
location / {
set $gitea_backend xflow_gitea:3000;
proxy_pass http://$gitea_backend;
proxy_http_version 1.1;
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 support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# ==========================================
# XFlow API
# ==========================================
server {
listen 80;
server_name api.* ~^api\..+$;
location / {
set $api_backend xflow_api:8080;
proxy_pass http://$api_backend;
proxy_http_version 1.1;
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;
# CORS
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
# ==========================================
# XFlow Dashboard (опционально)
# ==========================================
server {
listen 80;
server_name xflow.* ~^xflow\..+$;
location / {
root /usr/share/nginx/html/xflow;
try_files $uri $uri/ /index.html;
}
location /api/ {
set $api_backend xflow_api:8080;
proxy_pass http://$api_backend/;
proxy_http_version 1.1;
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;
}
}
# ==========================================
# WebDAV (если нужен прямой доступ)
# ==========================================
server {
listen 80;
server_name webdav.* ~^webdav\..+$;
location / {
set $webdav_backend xflow_webdav:80;
proxy_pass http://$webdav_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebDAV methods
dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS;
client_max_body_size 100M;
client_body_temp_path /tmp;
}
}
# ==========================================
# Map для WebSocket connection upgrade
# ==========================================
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}