feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI
This commit is contained in:
@@ -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
|
||||
@@ -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=
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}]
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
# }
|
||||
# }
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 "$@"
|
||||
@@ -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 "$@"
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user