Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
361
deployment/windows/README.md
Normal file
361
deployment/windows/README.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# ROA2WEB - Windows Deployment Package
|
||||
|
||||
Complete deployment solution for ROA2WEB on Windows Server with IIS and Oracle Database.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Package Contents
|
||||
|
||||
```
|
||||
deployment/windows/
|
||||
├── config/ # Configuration files
|
||||
│ ├── web.config # IIS configuration (URL Rewrite, reverse proxy)
|
||||
│ └── .env.production.windows # Environment variables template
|
||||
│
|
||||
├── scripts/ # PowerShell automation scripts
|
||||
│ ├── Install-ROA2WEB.ps1 # Initial installation
|
||||
│ ├── Deploy-ROA2WEB.ps1 # Deploy updates
|
||||
│ ├── Build-Frontend.ps1 # Build Vue.js frontend (run locally)
|
||||
│ ├── Start-ROA2WEB.ps1 # Start backend service
|
||||
│ ├── Stop-ROA2WEB.ps1 # Stop backend service
|
||||
│ └── Restart-ROA2WEB.ps1 # Restart backend service
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
│ └── WINDOWS_DEPLOYMENT.md # Complete deployment guide
|
||||
│
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Windows Server** 2016+ (or Windows 10/11 Pro)
|
||||
- **IIS** installed
|
||||
- **Oracle Database** (local or network-accessible)
|
||||
- **PowerShell 5.1+**
|
||||
- **Administrator privileges**
|
||||
|
||||
### Installation Steps
|
||||
|
||||
#### 1. Build Frontend (on development machine)
|
||||
|
||||
```bash
|
||||
# On WSL/Linux/Mac
|
||||
cd roa2web/deployment/windows/scripts
|
||||
./Build-Frontend.ps1
|
||||
|
||||
# This creates: ./deploy-package/
|
||||
```
|
||||
|
||||
#### 2. Transfer to Server
|
||||
|
||||
Copy the entire project to Windows Server:
|
||||
```
|
||||
C:\roa2web\deployment\windows\
|
||||
```
|
||||
|
||||
#### 3. Run Installation
|
||||
|
||||
```powershell
|
||||
# On Windows Server (PowerShell as Administrator)
|
||||
cd C:\roa2web\deployment\windows\scripts
|
||||
|
||||
# Install everything
|
||||
.\Install-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
This will:
|
||||
- ✅ Install Python 3.11+
|
||||
- ✅ Install NSSM (service manager)
|
||||
- ✅ Install IIS URL Rewrite and ARR
|
||||
- ✅ Create directory structure
|
||||
- ✅ Install Python dependencies
|
||||
- ✅ Create Windows Service
|
||||
- ✅ Configure IIS website
|
||||
|
||||
#### 4. Configure Application
|
||||
|
||||
```powershell
|
||||
# Copy and edit environment file
|
||||
Copy-Item C:\inetpub\wwwroot\roa2web\backend\config\.env.production.windows `
|
||||
C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
# Edit with your values
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
```
|
||||
|
||||
**Required settings:**
|
||||
|
||||
Configure these variables in `.env`:
|
||||
- Database credentials (user, password, host, port, SID)
|
||||
- JWT secret key for authentication
|
||||
- Other application-specific settings
|
||||
|
||||
Example structure:
|
||||
```env
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
# Add password and JWT secret here
|
||||
```
|
||||
|
||||
#### 5. Deploy Application Files
|
||||
|
||||
```powershell
|
||||
# Deploy frontend and backend
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\path\to\deploy-package"
|
||||
```
|
||||
|
||||
#### 6. Verify Installation
|
||||
|
||||
```powershell
|
||||
# Check service
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Test backend
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
|
||||
# Open application
|
||||
Start-Process "http://localhost"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Update Workflow
|
||||
|
||||
For deploying updates to existing installation:
|
||||
|
||||
**1. Build on development machine:**
|
||||
```bash
|
||||
cd roa2web/deployment/windows/scripts
|
||||
./Build-Frontend.ps1 -OutputPath "./deploy-$(date +%Y%m%d)"
|
||||
```
|
||||
|
||||
**2. Transfer to server:**
|
||||
```powershell
|
||||
Copy-Item .\deploy-20250118 -Destination C:\Temp\roa2web-deploy -Recurse
|
||||
```
|
||||
|
||||
**3. Deploy on server:**
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\deployment\windows\scripts
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\Temp\roa2web-deploy"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Management Commands
|
||||
|
||||
```powershell
|
||||
# Start backend service
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
# Stop backend service
|
||||
.\Stop-ROA2WEB.ps1
|
||||
|
||||
# Restart backend service
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# View logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
|
||||
# Check service status
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Check IIS website
|
||||
Get-Website ROA2WEB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Architecture
|
||||
|
||||
### Components
|
||||
|
||||
| Component | Type | Port | Purpose |
|
||||
|-----------|------|------|---------|
|
||||
| **Frontend** | IIS Static Files | 80/443 | Vue.js SPA |
|
||||
| **Backend** | Windows Service | 8000 | FastAPI API |
|
||||
| **Database** | Oracle | 1521 | Data storage |
|
||||
| **Reverse Proxy** | IIS URL Rewrite | - | API routing |
|
||||
|
||||
### Network Flow
|
||||
|
||||
```
|
||||
Client → IIS (port 80) → [web.config URL Rewrite]
|
||||
├─ /api/* → Backend Service (localhost:8000)
|
||||
│ ↓
|
||||
│ Oracle DB (localhost:1521)
|
||||
└─ /* → Static Files (Vue.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Directory Structure After Installation
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── backend\ # FastAPI application
|
||||
│ ├── app\
|
||||
│ ├── requirements.txt
|
||||
│ ├── .env # Configuration
|
||||
│ └── logs\
|
||||
│
|
||||
├── frontend\ # Vue.js static files
|
||||
│ ├── index.html
|
||||
│ ├── assets\
|
||||
│ └── web.config
|
||||
│
|
||||
├── logs\ # Service logs
|
||||
│ ├── backend-stdout.log
|
||||
│ └── backend-stderr.log
|
||||
│
|
||||
└── backups\ # Automatic backups
|
||||
└── backup-YYYYMMDD-HHMMSS\
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
```powershell
|
||||
# Check logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 50
|
||||
|
||||
# Test manually
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
### Frontend not loading
|
||||
|
||||
```powershell
|
||||
# Restart IIS
|
||||
iisreset
|
||||
|
||||
# Check website status
|
||||
Get-Website ROA2WEB
|
||||
Start-Website ROA2WEB
|
||||
```
|
||||
|
||||
### API calls failing (502/504)
|
||||
|
||||
```powershell
|
||||
# Check backend service
|
||||
Get-Service ROA2WEB-Backend
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# Test backend directly
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
```
|
||||
|
||||
### Database connection issues
|
||||
|
||||
```powershell
|
||||
# Test Oracle connection
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
|
||||
# Check Oracle service
|
||||
Get-Service Oracle*
|
||||
|
||||
# Check .env configuration
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\backend\.env | Select-String ORACLE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Full Documentation
|
||||
|
||||
For complete documentation, see:
|
||||
- **[WINDOWS_DEPLOYMENT.md](docs/WINDOWS_DEPLOYMENT.md)** - Comprehensive deployment guide
|
||||
- **[.env.production.windows](config/.env.production.windows)** - Configuration reference
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Features
|
||||
|
||||
✅ **Simple Installation** - One PowerShell script installs everything
|
||||
✅ **Minimal Dependencies** - Only Python + IIS (already on Windows Server)
|
||||
✅ **Easy Replication** - Same scripts work on all servers
|
||||
✅ **Automatic Backups** - Every deployment creates a backup
|
||||
✅ **Windows Service** - Backend runs as service with auto-start/restart
|
||||
✅ **Production Ready** - Optimized for performance and reliability
|
||||
|
||||
---
|
||||
|
||||
## 📊 System Requirements
|
||||
|
||||
| Resource | Minimum | Recommended |
|
||||
|----------|---------|-------------|
|
||||
| **OS** | Windows Server 2016 | Windows Server 2019+ |
|
||||
| **RAM** | 4 GB | 8 GB |
|
||||
| **CPU** | 2 cores | 4 cores |
|
||||
| **Disk** | 10 GB free | 20 GB free |
|
||||
| **Network** | 100 Mbps | 1 Gbps |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Recommendations
|
||||
|
||||
1. **Generate Strong JWT Secret:**
|
||||
```powershell
|
||||
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | % {[char]$_})
|
||||
```
|
||||
|
||||
2. **Secure .env File:**
|
||||
```powershell
|
||||
icacls C:\inetpub\wwwroot\roa2web\backend\.env /inheritance:r /grant:r Administrators:F
|
||||
```
|
||||
|
||||
3. **Enable HTTPS:** ⭐ **RECOMMENDED**
|
||||
```powershell
|
||||
# Quick setup with automated script
|
||||
cd C:\roa2web\deployment\windows\scripts
|
||||
.\Enable-HTTPS.ps1
|
||||
|
||||
# For detailed instructions, see:
|
||||
# docs/HTTPS_SETUP.md
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Creates/installs SSL certificate
|
||||
- Configures HTTPS binding (port 443)
|
||||
- Enables HTTP to HTTPS redirect
|
||||
- Activates HSTS (Strict Transport Security)
|
||||
|
||||
**Access your application securely:**
|
||||
- `https://10.0.20.36/roa2web` (or your domain)
|
||||
|
||||
4. **Regular Updates:**
|
||||
- Keep Windows Server updated
|
||||
- Update Python packages monthly
|
||||
- Monitor security advisories
|
||||
- Renew SSL certificates before expiry
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `C:\inetpub\wwwroot\roa2web\logs\`
|
||||
2. Review [WINDOWS_DEPLOYMENT.md](docs/WINDOWS_DEPLOYMENT.md)
|
||||
3. Contact: development-team@your-company.com
|
||||
|
||||
---
|
||||
|
||||
## 📝 Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 2.0.0 | 2025-01-18 | Initial Windows deployment package |
|
||||
|
||||
---
|
||||
|
||||
*ROA2WEB - Modern ERP Reports Application*
|
||||
*Windows Server Deployment Package v2.0.0*
|
||||
158
deployment/windows/config/web.config
Normal file
158
deployment/windows/config/web.config
Normal file
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
ROA2WEB - IIS Web Configuration
|
||||
|
||||
This configuration enables:
|
||||
- SPA routing for Vue.js (all routes fallback to index.html)
|
||||
- Reverse proxy for /api/* to backend FastAPI service (localhost:8000)
|
||||
- Compression and caching for optimal performance
|
||||
- Security headers
|
||||
|
||||
Prerequisites:
|
||||
- IIS URL Rewrite Module: https://www.iis.net/downloads/microsoft/url-rewrite
|
||||
- IIS Application Request Routing (ARR): https://www.iis.net/downloads/microsoft/application-request-routing
|
||||
-->
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
|
||||
<!-- Static Content Compression -->
|
||||
<urlCompression doStaticCompression="true" doDynamicCompression="true" />
|
||||
|
||||
<!-- Default Document -->
|
||||
<defaultDocument>
|
||||
<files>
|
||||
<clear />
|
||||
<add value="index.html" />
|
||||
</files>
|
||||
</defaultDocument>
|
||||
|
||||
<!-- Static Content Settings -->
|
||||
<staticContent>
|
||||
<!-- Enable MIME types for modern web assets -->
|
||||
<!-- Remove first to avoid duplicates, then add -->
|
||||
<remove fileExtension=".json" />
|
||||
<mimeMap fileExtension=".json" mimeType="application/json" />
|
||||
<remove fileExtension=".woff" />
|
||||
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
|
||||
<remove fileExtension=".woff2" />
|
||||
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
|
||||
<remove fileExtension=".svg" />
|
||||
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
|
||||
<remove fileExtension=".webmanifest" />
|
||||
<mimeMap fileExtension=".webmanifest" mimeType="application/manifest+json" />
|
||||
|
||||
<!-- Client-side caching for static assets -->
|
||||
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" />
|
||||
</staticContent>
|
||||
|
||||
<!-- Custom HTTP Headers (Security) -->
|
||||
<httpProtocol>
|
||||
<customHeaders>
|
||||
<!-- Security Headers -->
|
||||
<add name="X-Frame-Options" value="DENY" />
|
||||
<add name="X-Content-Type-Options" value="nosniff" />
|
||||
<add name="X-XSS-Protection" value="1; mode=block" />
|
||||
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
|
||||
<add name="Permissions-Policy" value="geolocation=(), microphone=(), camera=()" />
|
||||
|
||||
<!-- Content Security Policy (adjust as needed) -->
|
||||
<add name="Content-Security-Policy" value="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' ws: wss:" />
|
||||
|
||||
<!-- Remove Server header for security -->
|
||||
<remove name="X-Powered-By" />
|
||||
</customHeaders>
|
||||
</httpProtocol>
|
||||
|
||||
<!-- URL Rewrite Rules -->
|
||||
<rewrite>
|
||||
<rules>
|
||||
|
||||
<!-- Rule 1: Force HTTPS (redirect HTTP to HTTPS) -->
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 2: Reverse Proxy for API Requests -->
|
||||
<rule name="API Reverse Proxy" stopProcessing="true">
|
||||
<match url="^api/(.*)" />
|
||||
<action type="Rewrite" url="http://localhost:8000/api/{R:1}" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 3: Health Check Endpoint -->
|
||||
<rule name="Health Check Proxy" stopProcessing="true">
|
||||
<match url="^health$" />
|
||||
<action type="Rewrite" url="http://localhost:8000/health" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 4: Don't rewrite if file exists (static assets) -->
|
||||
<rule name="StaticContent" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" />
|
||||
</conditions>
|
||||
<action type="None" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 5: Don't rewrite if directory exists -->
|
||||
<rule name="StaticDirectory" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" />
|
||||
</conditions>
|
||||
<action type="None" />
|
||||
</rule>
|
||||
|
||||
<!-- Rule 6: SPA Routing - Rewrite all other requests to index.html -->
|
||||
<rule name="SPA Fallback" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions logicalGrouping="MatchAll">
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
<add input="{REQUEST_URI}" pattern="^/api" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="index.html" />
|
||||
</rule>
|
||||
|
||||
</rules>
|
||||
|
||||
<!-- Outbound Rules (optional - for modifying responses) -->
|
||||
<outboundRules>
|
||||
<rule name="Add HSTS Header" preCondition="IsHTTPS">
|
||||
<match serverVariable="RESPONSE_Strict-Transport-Security" pattern=".*" />
|
||||
<action type="Rewrite" value="max-age=31536000; includeSubDomains" />
|
||||
</rule>
|
||||
<preConditions>
|
||||
<preCondition name="IsHTTPS">
|
||||
<add input="{HTTPS}" pattern="on" />
|
||||
</preCondition>
|
||||
</preConditions>
|
||||
</outboundRules>
|
||||
</rewrite>
|
||||
|
||||
<!-- Error Pages -->
|
||||
<httpErrors errorMode="Custom" existingResponse="Replace">
|
||||
<!-- 404 - Not Found: Serve index.html for SPA routing -->
|
||||
<remove statusCode="404" subStatusCode="-1" />
|
||||
<error statusCode="404" path="index.html" responseMode="ExecuteURL" />
|
||||
|
||||
<!-- 500 - Internal Server Error -->
|
||||
<remove statusCode="500" subStatusCode="-1" />
|
||||
<error statusCode="500" path="index.html" responseMode="ExecuteURL" />
|
||||
</httpErrors>
|
||||
|
||||
<!-- Disable directory browsing -->
|
||||
<directoryBrowse enabled="false" />
|
||||
|
||||
</system.webServer>
|
||||
|
||||
<!-- System.web for ASP.NET settings (if needed) -->
|
||||
<system.web>
|
||||
<compilation debug="false" targetFramework="4.8" />
|
||||
<httpRuntime targetFramework="4.8" maxRequestLength="10240" executionTimeout="300" />
|
||||
</system.web>
|
||||
|
||||
</configuration>
|
||||
572
deployment/windows/docs/HTTPS_SETUP.md
Normal file
572
deployment/windows/docs/HTTPS_SETUP.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# HTTPS Setup Guide for ROA2WEB on IIS
|
||||
|
||||
Complete guide for enabling HTTPS on ROA2WEB deployed on Windows Server with IIS.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Start
|
||||
|
||||
### Option 1: Automated Setup (Recommended)
|
||||
|
||||
Run the automated PowerShell script:
|
||||
|
||||
```powershell
|
||||
# On Windows Server (PowerShell as Administrator)
|
||||
cd C:\path\to\roa2web\deployment\windows\scripts
|
||||
|
||||
# Enable HTTPS with self-signed certificate
|
||||
.\Enable-HTTPS.ps1
|
||||
|
||||
# Or specify custom settings
|
||||
.\Enable-HTTPS.ps1 -IISSiteName "Default Web Site" -CertificateDnsName "10.0.20.36"
|
||||
```
|
||||
|
||||
### Option 2: Manual Setup
|
||||
|
||||
Follow the step-by-step instructions in the [Manual Configuration](#manual-configuration) section below.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Certificate Options
|
||||
|
||||
### Self-Signed Certificate (Development/Testing)
|
||||
|
||||
**Pros:**
|
||||
- Quick setup (5 minutes)
|
||||
- No cost
|
||||
- Works immediately
|
||||
|
||||
**Cons:**
|
||||
- Browser security warnings
|
||||
- Not trusted by default
|
||||
- Not recommended for production
|
||||
|
||||
**Use when:**
|
||||
- Internal development
|
||||
- Testing HTTPS functionality
|
||||
- Private internal network
|
||||
|
||||
### CA-Issued Certificate (Production)
|
||||
|
||||
**Pros:**
|
||||
- Trusted by all browsers
|
||||
- No security warnings
|
||||
- Professional appearance
|
||||
|
||||
**Cons:**
|
||||
- Requires domain name
|
||||
- May have cost (unless using Let's Encrypt)
|
||||
- More setup steps
|
||||
|
||||
**Use when:**
|
||||
- Production deployment
|
||||
- Public-facing application
|
||||
- Customer/client access
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Automated Setup Details
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Windows Server 2016+ (or Windows 10/11 Pro)
|
||||
- IIS installed and configured
|
||||
- Administrator privileges
|
||||
- ROA2WEB already deployed
|
||||
|
||||
### Running the Script
|
||||
|
||||
**Basic usage (auto-detect settings):**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Auto-detect your server's hostname and IP
|
||||
2. Create a self-signed certificate
|
||||
3. Configure HTTPS binding on IIS
|
||||
4. Enable HTTP to HTTPS redirect
|
||||
5. Test the configuration
|
||||
|
||||
**Custom DNS name:**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1 -CertificateDnsName "roa2web.company.com"
|
||||
```
|
||||
|
||||
**Use existing certificate:**
|
||||
|
||||
```powershell
|
||||
# List available certificates
|
||||
Get-ChildItem cert:\LocalMachine\My | Select-Object Thumbprint, Subject, FriendlyName
|
||||
|
||||
# Use specific certificate
|
||||
.\Enable-HTTPS.ps1 -UseExistingCert -CertThumbprint "ABC123..."
|
||||
```
|
||||
|
||||
**For IIS application (not site root):**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1 -IISSiteName "Default Web Site"
|
||||
```
|
||||
|
||||
**Custom HTTPS port:**
|
||||
|
||||
```powershell
|
||||
.\Enable-HTTPS.ps1 -Port 8443
|
||||
```
|
||||
|
||||
### Script Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `IISSiteName` | String | "Default Web Site" | IIS site name |
|
||||
| `CertificateDnsName` | String | Auto-detect | DNS name for certificate |
|
||||
| `UseExistingCert` | Switch | False | Use existing certificate |
|
||||
| `CertThumbprint` | String | "" | Thumbprint of existing cert |
|
||||
| `Port` | Int | 443 | HTTPS port |
|
||||
| `IPAddress` | String | "*" | Bind to specific IP or all (*) |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Manual Configuration
|
||||
|
||||
### Step 1: Create SSL Certificate
|
||||
|
||||
#### Option A: Self-Signed Certificate
|
||||
|
||||
```powershell
|
||||
# Create certificate for IP address (10.0.20.36)
|
||||
$cert = New-SelfSignedCertificate `
|
||||
-DnsName "10.0.20.36" `
|
||||
-CertStoreLocation "cert:\LocalMachine\My" `
|
||||
-NotAfter (Get-Date).AddYears(5) `
|
||||
-FriendlyName "ROA2WEB SSL Certificate" `
|
||||
-KeyUsage DigitalSignature, KeyEncipherment `
|
||||
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")
|
||||
|
||||
# Display certificate info
|
||||
$cert | Select-Object Subject, Thumbprint, NotAfter
|
||||
```
|
||||
|
||||
#### Option B: CA-Issued Certificate
|
||||
|
||||
**Using IIS Manager:**
|
||||
|
||||
1. Open IIS Manager (`inetmgr`)
|
||||
2. Select your server in the left panel
|
||||
3. Double-click "Server Certificates"
|
||||
4. Click "Create Certificate Request..." in right panel
|
||||
5. Fill in certificate details:
|
||||
- Common Name: `10.0.20.36` (or your domain)
|
||||
- Organization: Your company name
|
||||
- City, State, Country
|
||||
6. Save the CSR file
|
||||
7. Submit CSR to Certificate Authority (CA)
|
||||
8. Once received, import certificate:
|
||||
- Click "Complete Certificate Request..."
|
||||
- Browse to certificate file
|
||||
- Give it a friendly name: "ROA2WEB SSL Certificate"
|
||||
|
||||
**Popular Certificate Authorities:**
|
||||
- **Let's Encrypt** (Free, automated): https://letsencrypt.org/
|
||||
- **DigiCert** (Commercial): https://www.digicert.com/
|
||||
- **Sectigo** (Commercial): https://sectigo.com/
|
||||
- **GlobalSign** (Commercial): https://www.globalsign.com/
|
||||
|
||||
### Step 2: Configure HTTPS Binding
|
||||
|
||||
```powershell
|
||||
Import-Module WebAdministration
|
||||
|
||||
# Add HTTPS binding to site
|
||||
New-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443 -IPAddress "*"
|
||||
|
||||
# Attach certificate to binding
|
||||
$cert = Get-ChildItem -Path "cert:\LocalMachine\My" |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
|
||||
Push-Location
|
||||
Set-Location IIS:\SslBindings
|
||||
$cert | New-Item "0.0.0.0!443"
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
### Step 3: Enable HTTP to HTTPS Redirect
|
||||
|
||||
**Option A: Automatic (via script)**
|
||||
|
||||
The `Enable-HTTPS.ps1` script will offer to add the redirect rule automatically.
|
||||
|
||||
**Option B: Manual Edit**
|
||||
|
||||
Edit `C:\inetpub\wwwroot\roa2web\frontend\web.config`:
|
||||
|
||||
```xml
|
||||
<rewrite>
|
||||
<rules>
|
||||
<!-- Add this rule FIRST (before other rules) -->
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
<!-- Existing rules below... -->
|
||||
<rule name="API Reverse Proxy" stopProcessing="true">
|
||||
<!-- ... -->
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
```
|
||||
|
||||
**Option C: IIS Manager GUI**
|
||||
|
||||
1. Open IIS Manager
|
||||
2. Navigate to your site
|
||||
3. Double-click "URL Rewrite"
|
||||
4. Click "Add Rule(s)..." → "Blank rule"
|
||||
5. Configure:
|
||||
- Name: `Force HTTPS`
|
||||
- Match URL: `(.*)`
|
||||
- Conditions: Add condition
|
||||
- Input: `{HTTPS}`
|
||||
- Pattern: `off`
|
||||
- Action:
|
||||
- Type: `Redirect`
|
||||
- URL: `https://{HTTP_HOST}/{R:1}`
|
||||
- Redirect type: `Permanent (301)`
|
||||
|
||||
### Step 4: Test Configuration
|
||||
|
||||
```powershell
|
||||
# Restart IIS site
|
||||
Restart-Website "Default Web Site"
|
||||
|
||||
# Test HTTPS locally (self-signed cert)
|
||||
Invoke-WebRequest https://localhost -SkipCertificateCheck
|
||||
|
||||
# Test from browser
|
||||
Start-Process "https://10.0.20.36/roa2web"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing HTTPS Configuration
|
||||
|
||||
### Browser Testing
|
||||
|
||||
1. **Access via HTTPS:**
|
||||
```
|
||||
https://10.0.20.36/roa2web
|
||||
```
|
||||
|
||||
2. **Check for security warnings:**
|
||||
- **Self-signed cert**: You'll see a warning - this is normal
|
||||
- **CA-issued cert**: No warning - connection is secure
|
||||
|
||||
3. **Verify redirect:**
|
||||
- Try accessing: `http://10.0.20.36/roa2web`
|
||||
- Should automatically redirect to: `https://10.0.20.36/roa2web`
|
||||
|
||||
4. **Check console for mixed content:**
|
||||
- Open browser DevTools (F12)
|
||||
- Look for mixed content warnings
|
||||
- All resources should load over HTTPS
|
||||
|
||||
### PowerShell Testing
|
||||
|
||||
```powershell
|
||||
# Test HTTPS binding
|
||||
Get-WebBinding -Name "Default Web Site" -Protocol "https"
|
||||
|
||||
# Test certificate
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
$cert | Select-Object Subject, Thumbprint, NotAfter, DnsNameList
|
||||
|
||||
# Test HTTPS response
|
||||
Invoke-WebRequest https://localhost/roa2web -SkipCertificateCheck
|
||||
|
||||
# Test from external IP
|
||||
Invoke-WebRequest https://10.0.20.36/roa2web -SkipCertificateCheck
|
||||
|
||||
# View IIS SSL bindings
|
||||
netsh http show sslcert
|
||||
```
|
||||
|
||||
### Network Testing
|
||||
|
||||
```bash
|
||||
# Test from Linux/Mac client
|
||||
curl -k https://10.0.20.36/roa2web
|
||||
|
||||
# Check certificate details
|
||||
openssl s_client -connect 10.0.20.36:443 -servername 10.0.20.36
|
||||
|
||||
# Test redirect
|
||||
curl -I http://10.0.20.36/roa2web
|
||||
# Should return: HTTP/1.1 301 Moved Permanently
|
||||
# Location: https://10.0.20.36/roa2web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### HTTPS Not Working
|
||||
|
||||
**Symptom:** Can't access site via HTTPS
|
||||
|
||||
**Check:**
|
||||
```powershell
|
||||
# Verify HTTPS binding exists
|
||||
Get-WebBinding -Name "Default Web Site"
|
||||
|
||||
# Check if port 443 is listening
|
||||
netstat -ano | findstr :443
|
||||
|
||||
# View SSL certificate bindings
|
||||
netsh http show sslcert
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
```powershell
|
||||
# Remove and recreate binding
|
||||
Remove-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443
|
||||
New-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443
|
||||
|
||||
# Reattach certificate
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
Push-Location IIS:\SslBindings
|
||||
$cert | New-Item "0.0.0.0!443" -Force
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
### Certificate Warning in Browser
|
||||
|
||||
**Symptom:** Browser shows "Your connection is not private" or similar warning
|
||||
|
||||
**Cause:**
|
||||
- Self-signed certificate (not trusted by default)
|
||||
- Expired certificate
|
||||
- Hostname mismatch
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **For Development (self-signed):**
|
||||
- Click "Advanced" → "Proceed anyway"
|
||||
- This is expected behavior for self-signed certificates
|
||||
|
||||
2. **For Production:**
|
||||
- Replace with CA-issued certificate
|
||||
- Ensure certificate CN matches the URL you're accessing
|
||||
|
||||
3. **For Internal Network:**
|
||||
- Add certificate to Trusted Root CA:
|
||||
```powershell
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My\<THUMBPRINT>
|
||||
$store = Get-Item cert:\LocalMachine\Root
|
||||
$store.Open("ReadWrite")
|
||||
$store.Add($cert)
|
||||
$store.Close()
|
||||
```
|
||||
|
||||
### HTTP Not Redirecting to HTTPS
|
||||
|
||||
**Symptom:** HTTP URLs still work, no automatic redirect
|
||||
|
||||
**Check:**
|
||||
```powershell
|
||||
# Verify web.config has redirect rule
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\frontend\web.config |
|
||||
Select-String "Force HTTPS"
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- Ensure redirect rule is present in web.config
|
||||
- Verify rule is BEFORE other rewrite rules
|
||||
- Check rule is not disabled
|
||||
- Restart IIS site:
|
||||
```powershell
|
||||
Restart-Website "Default Web Site"
|
||||
```
|
||||
|
||||
### Mixed Content Warnings
|
||||
|
||||
**Symptom:** Console shows "Mixed Content" warnings
|
||||
|
||||
**Cause:** Some resources loading over HTTP instead of HTTPS
|
||||
|
||||
**Fix:**
|
||||
1. Check frontend code for hardcoded `http://` URLs
|
||||
2. Update to use relative URLs or `https://`
|
||||
3. Update API base URL in frontend config:
|
||||
```javascript
|
||||
// src/config.js or similar
|
||||
const API_BASE_URL = window.location.protocol === 'https:'
|
||||
? 'https://10.0.20.36/api'
|
||||
: 'http://10.0.20.36/api';
|
||||
```
|
||||
|
||||
### API Calls Failing After HTTPS
|
||||
|
||||
**Symptom:** Frontend loads but API calls fail with CORS or SSL errors
|
||||
|
||||
**Check:**
|
||||
```powershell
|
||||
# Verify backend is accessible
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
|
||||
# Check IIS URL Rewrite is forwarding correctly
|
||||
Get-WebConfiguration -Filter "system.webServer/rewrite/rules"
|
||||
```
|
||||
|
||||
**Fix:**
|
||||
- Update CORS settings in FastAPI backend to allow HTTPS origin
|
||||
- Verify web.config proxy rules are correct
|
||||
- Check backend logs for errors
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Best Practices
|
||||
|
||||
### 1. Use Strong Certificates
|
||||
|
||||
```powershell
|
||||
# For production, use CA-issued certificates
|
||||
# Minimum key size: 2048 bits (4096 recommended)
|
||||
# Use SHA-256 or higher
|
||||
```
|
||||
|
||||
### 2. Enable HSTS (Strict Transport Security)
|
||||
|
||||
Already configured in `web.config` (lines 115-124):
|
||||
|
||||
```xml
|
||||
<rule name="Add HSTS Header" preCondition="IsHTTPS">
|
||||
<match serverVariable="RESPONSE_Strict-Transport-Security" pattern=".*" />
|
||||
<action type="Rewrite" value="max-age=31536000; includeSubDomains" />
|
||||
</rule>
|
||||
```
|
||||
|
||||
This tells browsers to always use HTTPS for your site.
|
||||
|
||||
### 3. Disable Weak SSL/TLS Protocols
|
||||
|
||||
```powershell
|
||||
# Disable SSL 2.0, SSL 3.0, TLS 1.0, TLS 1.1
|
||||
# Enable only TLS 1.2 and TLS 1.3
|
||||
|
||||
# Run as Administrator
|
||||
New-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Force
|
||||
New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'Enabled' -Value 1 -PropertyType 'DWord'
|
||||
New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server' -Name 'DisabledByDefault' -Value 0 -PropertyType 'DWord'
|
||||
|
||||
# Restart required
|
||||
Restart-Computer
|
||||
```
|
||||
|
||||
### 4. Secure Cookie Settings
|
||||
|
||||
Update FastAPI backend cookie settings:
|
||||
|
||||
```python
|
||||
# backend/app/main.py
|
||||
response.set_cookie(
|
||||
key="access_token",
|
||||
value=token,
|
||||
httponly=True,
|
||||
secure=True, # Only send over HTTPS
|
||||
samesite="lax"
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Regular Certificate Renewal
|
||||
|
||||
- CA certificates typically expire in 1-2 years
|
||||
- Let's Encrypt certificates expire in 90 days
|
||||
- Set up reminders or automated renewal
|
||||
|
||||
```powershell
|
||||
# Check certificate expiry
|
||||
$cert = Get-ChildItem cert:\LocalMachine\My |
|
||||
Where-Object {$_.FriendlyName -eq "ROA2WEB SSL Certificate"}
|
||||
$cert.NotAfter
|
||||
|
||||
# Days until expiry
|
||||
($cert.NotAfter - (Get-Date)).Days
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Documentation
|
||||
- [IIS SSL Configuration](https://docs.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)
|
||||
- [Let's Encrypt](https://letsencrypt.org/)
|
||||
- [SSL/TLS Best Practices](https://wiki.mozilla.org/Security/Server_Side_TLS)
|
||||
|
||||
### Testing Tools
|
||||
- [SSL Labs Server Test](https://www.ssllabs.com/ssltest/) - Comprehensive SSL/TLS analysis
|
||||
- [SSL Checker](https://www.sslshopper.com/ssl-checker.html) - Quick certificate validation
|
||||
- [Why No Padlock?](https://www.whynopadlock.com/) - Find mixed content issues
|
||||
|
||||
### Certificate Providers
|
||||
- **Free:**
|
||||
- [Let's Encrypt](https://letsencrypt.org/) (Automated, 90-day validity)
|
||||
- [ZeroSSL](https://zerossl.com/) (Free tier available)
|
||||
|
||||
- **Commercial:**
|
||||
- [DigiCert](https://www.digicert.com/)
|
||||
- [Sectigo](https://sectigo.com/)
|
||||
- [GlobalSign](https://www.globalsign.com/)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```powershell
|
||||
# View all certificates
|
||||
Get-ChildItem cert:\LocalMachine\My
|
||||
|
||||
# View IIS bindings
|
||||
Get-WebBinding -Name "Default Web Site"
|
||||
|
||||
# View SSL bindings
|
||||
netsh http show sslcert
|
||||
|
||||
# Test HTTPS
|
||||
Invoke-WebRequest https://localhost -SkipCertificateCheck
|
||||
|
||||
# Restart IIS site
|
||||
Restart-Website "Default Web Site"
|
||||
|
||||
# Enable HTTPS (automated script)
|
||||
.\Enable-HTTPS.ps1
|
||||
```
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- **IIS bindings**: IIS Manager → Site → Bindings
|
||||
- **web.config**: `C:\inetpub\wwwroot\roa2web\frontend\web.config`
|
||||
- **Certificates**: Certificate Manager (`certmgr.msc`)
|
||||
- **Backend config**: `C:\inetpub\wwwroot\roa2web\backend\.env`
|
||||
|
||||
### Access Points
|
||||
|
||||
- **HTTP**: `http://10.0.20.36/roa2web` (redirects to HTTPS)
|
||||
- **HTTPS**: `https://10.0.20.36/roa2web`
|
||||
- **API**: `https://10.0.20.36/api/*` (proxied to backend)
|
||||
- **Health**: `https://10.0.20.36/health`
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-01-18*
|
||||
*ROA2WEB HTTPS Setup Guide v1.0*
|
||||
844
deployment/windows/docs/TELEGRAM_BOT_DEPLOYMENT.md
Normal file
844
deployment/windows/docs/TELEGRAM_BOT_DEPLOYMENT.md
Normal file
@@ -0,0 +1,844 @@
|
||||
# ROA2WEB Telegram Bot - Windows Server Deployment Guide
|
||||
|
||||
**Target Server**: Windows Server (10.0.20.36)
|
||||
**Deployment Method**: Windows Service (NSSM) - No Docker
|
||||
**Service Name**: ROA2WEB-TelegramBot
|
||||
**Internal API Port**: 8002
|
||||
**Created**: 2025-10-22
|
||||
**Last Updated**: 2025-10-22
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Deployment Architecture](#deployment-architecture)
|
||||
4. [Installation Steps](#installation-steps)
|
||||
5. [Configuration](#configuration)
|
||||
6. [Service Management](#service-management)
|
||||
7. [Database Backup](#database-backup)
|
||||
8. [Monitoring & Troubleshooting](#monitoring--troubleshooting)
|
||||
9. [Security Considerations](#security-considerations)
|
||||
10. [Maintenance Procedures](#maintenance-procedures)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The ROA2WEB Telegram Bot is deployed as a Windows Service on the same server as the backend (10.0.20.36). It provides an alternative conversational interface to ROA2WEB using Claude Agent SDK.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Conversational AI**: Claude Agent SDK with 5 custom tools
|
||||
- **Account Linking**: Secure linking between Telegram and Oracle accounts
|
||||
- **Real-time Queries**: Dashboard, invoices, treasury, exports
|
||||
- **Database**: Standalone SQLite for Telegram-specific data
|
||||
- **Internal API**: FastAPI endpoint for backend callbacks (port 8002)
|
||||
- **Production Ready**: All components use real backend integration (no mocks)
|
||||
|
||||
### Deployment Method
|
||||
|
||||
- **Windows Service**: Runs via NSSM (Non-Sucking Service Manager)
|
||||
- **No Docker**: Direct Windows deployment (same pattern as backend)
|
||||
- **Auto-start**: Service starts automatically on server boot
|
||||
- **Auto-recovery**: Service restarts automatically on failure
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Server Requirements
|
||||
|
||||
- **Operating System**: Windows Server 2016+ or Windows 10/11
|
||||
- **Python**: 3.11 or higher
|
||||
- **RAM**: Minimum 512 MB (dedicated to service)
|
||||
- **Disk Space**: Minimum 500 MB
|
||||
- **Network**: Access to localhost:8000 (backend API)
|
||||
|
||||
### Required Credentials
|
||||
|
||||
1. **Telegram Bot Token**
|
||||
- Get from @BotFather on Telegram
|
||||
- Command: `/newbot` or `/mybots`
|
||||
- Example: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`
|
||||
|
||||
2. **Claude Authentication** (Choose ONE method)
|
||||
|
||||
**Method A: Claude Pro/Max Subscription** ⭐ **RECOMMENDED**
|
||||
- ✅ **NO API key needed**
|
||||
- ✅ **NO additional costs** (included in your subscription)
|
||||
- ✅ Uses browser authentication
|
||||
- See: [Claude Authentication Setup](#claude-authentication-setup) below
|
||||
|
||||
**Method B: Claude API Key** (Alternative)
|
||||
- Get from Anthropic Console: https://console.anthropic.com/settings/keys
|
||||
- Example: `sk-ant-api03-XXXXXXXX...`
|
||||
- ⚠️ Usage-based billing applies
|
||||
- Takes precedence over Method A if both are configured
|
||||
|
||||
3. **Backend Access**
|
||||
- Backend should be running on http://localhost:8000
|
||||
- Verify: `Invoke-WebRequest http://localhost:8000/health`
|
||||
|
||||
### Software Prerequisites
|
||||
|
||||
- **PowerShell 5.1+**: Built into Windows Server
|
||||
- **Python 3.11+**: Will be installed if missing (via Chocolatey)
|
||||
- **NSSM**: Will be installed automatically by installation script
|
||||
|
||||
---
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\telegram-bot\
|
||||
├── app\ # Application source code
|
||||
│ ├── main.py # Entry point, Claude Agent wrapper
|
||||
│ ├── bot\ # Telegram bot handlers
|
||||
│ ├── agent\ # Claude Agent tools and sessions
|
||||
│ ├── auth\ # Account linking logic
|
||||
│ ├── db\ # SQLite database operations
|
||||
│ ├── api\ # Backend API client
|
||||
│ └── internal_api.py # FastAPI internal API
|
||||
├── venv\ # Python virtual environment
|
||||
├── data\ # SQLite database
|
||||
│ └── telegram_bot.db # Main database file
|
||||
├── logs\ # Application logs
|
||||
│ ├── stdout.log # Service output
|
||||
│ ├── stderr.log # Service errors
|
||||
│ └── backup.log # Backup operations
|
||||
├── backups\ # Database backups
|
||||
├── temp\ # Temporary files
|
||||
├── scripts\ # PowerShell management scripts
|
||||
├── config\ # Configuration templates
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Environment configuration
|
||||
```
|
||||
|
||||
### Service Integration
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Windows Server 10.0.20.36 │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ IIS (Port 80) │ │ ROA2WEB-Backend │ │
|
||||
│ │ Frontend Static │ │ (Port 8000) │ │
|
||||
│ └──────────────────┘ └────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ │ HTTP API Calls │
|
||||
│ │ │
|
||||
│ ┌─────────────────▼──────────────┐ │
|
||||
│ │ ROA2WEB-TelegramBot │ │
|
||||
│ │ (Port 8002 - Internal API) │ │
|
||||
│ │ - Telegram Bot Handlers │ │
|
||||
│ │ - Claude Agent SDK │ │
|
||||
│ │ - SQLite Database │ │
|
||||
│ └────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Telegram Bot API
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Telegram │
|
||||
│ Cloud │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### Step 1: Build Deployment Package (Development Machine)
|
||||
|
||||
On your development machine (WSL/Linux), run:
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/deployment/windows/scripts
|
||||
./Build-TelegramBot.ps1
|
||||
```
|
||||
|
||||
This creates the deployment package at:
|
||||
```
|
||||
../deploy-package/telegram-bot/
|
||||
```
|
||||
|
||||
**Package Contents**:
|
||||
- `app/` - Application source code
|
||||
- `requirements.txt` - Python dependencies
|
||||
- `.env.example` - Configuration template
|
||||
- `scripts/` - PowerShell management scripts
|
||||
- `config/` - Production config templates
|
||||
- `README.txt` - Deployment instructions
|
||||
|
||||
### Step 2: Transfer Package to Server
|
||||
|
||||
**Option A: Network Share** (Recommended)
|
||||
```powershell
|
||||
# On development machine
|
||||
Copy-Item -Path ./deploy-package/telegram-bot -Destination \\10.0.20.36\C$\Temp\telegram-bot-deploy -Recurse
|
||||
```
|
||||
|
||||
**Option B: RDP**
|
||||
1. Connect to server via RDP: `mstsc /v:10.0.20.36`
|
||||
2. Manually copy deployment package to `C:\Temp\telegram-bot-deploy`
|
||||
|
||||
### Step 3: Run Installation Script
|
||||
|
||||
On Windows Server (10.0.20.36), open PowerShell as Administrator:
|
||||
|
||||
```powershell
|
||||
cd C:\Temp\telegram-bot-deploy\scripts
|
||||
.\Install-TelegramBot.ps1
|
||||
```
|
||||
|
||||
**Installation Process**:
|
||||
1. ✅ Checks Python 3.11+ installation
|
||||
2. ✅ Installs NSSM (service manager)
|
||||
3. ✅ Creates directory structure
|
||||
4. ✅ Creates Python virtual environment
|
||||
5. ✅ Installs Python dependencies
|
||||
6. ✅ Creates Windows Service (ROA2WEB-TelegramBot)
|
||||
7. ✅ Creates .env configuration template
|
||||
|
||||
**Installation Output**:
|
||||
```
|
||||
====================================================================
|
||||
ROA2WEB TELEGRAM BOT INSTALLATION COMPLETED
|
||||
====================================================================
|
||||
|
||||
Installation Details:
|
||||
Install Path: C:\inetpub\wwwroot\roa2web\telegram-bot
|
||||
Service Name: ROA2WEB-TelegramBot
|
||||
Internal API Port: 8002
|
||||
|
||||
Next Steps:
|
||||
1. Edit configuration: C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
2. Start service: .\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Step 4: Configure Environment
|
||||
|
||||
Edit the `.env` file:
|
||||
|
||||
```powershell
|
||||
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
```
|
||||
|
||||
**Required Configuration**:
|
||||
```env
|
||||
# CRITICAL: Update this value
|
||||
TELEGRAM_BOT_TOKEN=your_production_bot_token_from_@BotFather
|
||||
|
||||
# Claude Authentication: Leave empty to use Claude Pro/Max subscription
|
||||
CLAUDE_API_KEY=
|
||||
|
||||
# Verify these are correct
|
||||
BACKEND_URL=http://localhost:8000
|
||||
SQLITE_DB_PATH=C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
INTERNAL_API_PORT=8002
|
||||
LOG_LEVEL=INFO
|
||||
ENVIRONMENT=production
|
||||
```
|
||||
|
||||
**Get Bot Token**:
|
||||
1. Open Telegram, search for `@BotFather`
|
||||
2. Send `/newbot` or `/mybots`
|
||||
3. Copy token to `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
### Step 4a: Claude Authentication Setup
|
||||
|
||||
**Choose ONE authentication method:**
|
||||
|
||||
#### **Method A: Claude Pro/Max Subscription** ⭐ **RECOMMENDED**
|
||||
|
||||
If you have a Claude Pro or Claude Max subscription, use this method (NO API key needed!):
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Setup-ClaudeAuth.ps1
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Script installs `claude-code` CLI (if not already installed)
|
||||
2. Opens your browser for authentication
|
||||
3. Log in with your Claude Pro/Max account
|
||||
4. Authorize the application
|
||||
5. Credentials are saved to: `%APPDATA%\claude\credentials.json`
|
||||
|
||||
**Expected Output:**
|
||||
```
|
||||
====================================================================
|
||||
IMPORTANT: Browser Authentication Required
|
||||
====================================================================
|
||||
|
||||
1. A browser window will open
|
||||
2. Log in with your Claude Pro/Max account
|
||||
3. Authorize the application
|
||||
4. Return to this window after authentication
|
||||
|
||||
====================================================================
|
||||
|
||||
[*] Opening browser for authentication...
|
||||
[OK] Authentication successful!
|
||||
[OK] Credentials file found and valid
|
||||
[OK] .env will use Claude Pro subscription (browser login)
|
||||
|
||||
====================================================================
|
||||
CLAUDE AUTHENTICATION SETUP COMPLETE
|
||||
====================================================================
|
||||
```
|
||||
|
||||
**Alternative: Copy credentials from development machine**
|
||||
|
||||
If the server doesn't have browser access, authenticate on your local machine and copy credentials:
|
||||
|
||||
```powershell
|
||||
# On local machine (with browser):
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
claude-code login
|
||||
|
||||
# Copy credentials file to server
|
||||
Copy-Item "$env:APPDATA\claude\credentials.json" -Destination "\\10.0.20.36\C$\Temp\claude-credentials.json"
|
||||
|
||||
# On server:
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Setup-ClaudeAuth.ps1 -Method copy -CredentialsPath "C:\Temp\claude-credentials.json"
|
||||
```
|
||||
|
||||
#### **Method B: Claude API Key** (Alternative)
|
||||
|
||||
If you prefer to use an API key instead:
|
||||
|
||||
1. Visit https://console.anthropic.com/settings/keys
|
||||
2. Create new key
|
||||
3. Edit `.env` and set `CLAUDE_API_KEY=sk-ant-api03-XXXXXXXX...`
|
||||
4. ⚠️ Usage-based billing applies
|
||||
|
||||
**Note:** API key takes precedence over browser login if both are configured.
|
||||
|
||||
### Step 5: Start Service
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
**Expected Output**:
|
||||
```
|
||||
[*] Starting ROA2WEB Telegram Bot Service...
|
||||
[*] Service start command issued
|
||||
[*] Waiting for service to start... (5/30)
|
||||
[OK] Service started successfully
|
||||
[OK] Health check passed: healthy
|
||||
[OK] Database: connected
|
||||
```
|
||||
|
||||
### Step 6: Verify Installation
|
||||
|
||||
**Check Service Status**:
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
```
|
||||
|
||||
Expected: `Status = Running`
|
||||
|
||||
**Check Health Endpoint**:
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health | ConvertFrom-Json
|
||||
```
|
||||
|
||||
Expected Output:
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": "2025-10-22T14:30:00",
|
||||
"database": {
|
||||
"status": "connected",
|
||||
"users": 0,
|
||||
"pending_codes": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test Bot on Telegram**:
|
||||
1. Open Telegram
|
||||
2. Search for your bot (name from @BotFather)
|
||||
3. Send `/start`
|
||||
4. Expected: Welcome message with linking instructions
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables Reference
|
||||
|
||||
See `.env.production.windows.telegram` in `config/` directory for complete reference.
|
||||
|
||||
**Key Settings**:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `TELEGRAM_BOT_TOKEN` | *required* | Bot token from @BotFather |
|
||||
| `CLAUDE_API_KEY` | *required* | API key from Anthropic |
|
||||
| `BACKEND_URL` | `http://localhost:8000` | Backend API URL |
|
||||
| `SQLITE_DB_PATH` | `C:\...\telegram_bot.db` | Database file location |
|
||||
| `INTERNAL_API_PORT` | `8002` | Internal API port |
|
||||
| `LOG_LEVEL` | `INFO` | Logging level (DEBUG/INFO/WARN/ERROR) |
|
||||
| `ENVIRONMENT` | `production` | Environment name |
|
||||
| `AUTH_CODE_EXPIRY_MINUTES` | `15` | Linking code expiry time |
|
||||
| `SESSION_TIMEOUT_MINUTES` | `60` | User session timeout |
|
||||
| `MAX_CONVERSATION_HISTORY` | `20` | Max messages per session |
|
||||
|
||||
### Applying Configuration Changes
|
||||
|
||||
After editing `.env`:
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Restart-TelegramBot.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Management
|
||||
|
||||
### Start Service
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Stop Service
|
||||
|
||||
```powershell
|
||||
.\Stop-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Restart Service
|
||||
|
||||
```powershell
|
||||
.\Restart-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### Check Service Status
|
||||
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
|
||||
# Detailed info
|
||||
Get-Service ROA2WEB-TelegramBot | Select-Object *
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
**Real-time Logs** (tail -f equivalent):
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log -Tail 50 -Wait
|
||||
```
|
||||
|
||||
**Error Logs**:
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
|
||||
```
|
||||
|
||||
**Backup Logs**:
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\backup.log -Tail 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Backup
|
||||
|
||||
### Manual Backup
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Backup-TelegramDB.ps1
|
||||
```
|
||||
|
||||
**Output**:
|
||||
- Backup file: `backups/telegram_bot_backup_YYYYMMDD-HHMMSS.db.zip`
|
||||
- Compressed and timestamped
|
||||
- Integrity tested automatically
|
||||
|
||||
### Setup Automated Daily Backup
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Setup-DailyBackup.ps1
|
||||
```
|
||||
|
||||
**Configuration**:
|
||||
- Runs daily at 2:00 AM
|
||||
- Keeps last 30 days of backups
|
||||
- Runs as SYSTEM account
|
||||
- Logs all operations
|
||||
|
||||
**Verify Scheduled Task**:
|
||||
```powershell
|
||||
Get-ScheduledTask -TaskName "ROA2WEB-TelegramBot-Backup"
|
||||
```
|
||||
|
||||
**Run Backup Manually** (via Task Scheduler):
|
||||
```powershell
|
||||
Start-ScheduledTask -TaskName "ROA2WEB-TelegramBot-Backup"
|
||||
```
|
||||
|
||||
### Restore from Backup
|
||||
|
||||
1. Stop service:
|
||||
```powershell
|
||||
.\Stop-TelegramBot.ps1
|
||||
```
|
||||
|
||||
2. Find backup file:
|
||||
```powershell
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\telegram-bot\backups | Sort-Object LastWriteTime -Descending
|
||||
```
|
||||
|
||||
3. Extract backup (if compressed):
|
||||
```powershell
|
||||
Expand-Archive -Path "backups\telegram_bot_backup_YYYYMMDD-HHMMSS.db.zip" -DestinationPath "temp\"
|
||||
```
|
||||
|
||||
4. Replace database:
|
||||
```powershell
|
||||
Copy-Item -Path "temp\telegram_bot_backup_YYYYMMDD-HHMMSS.db" -Destination "data\telegram_bot.db" -Force
|
||||
```
|
||||
|
||||
5. Start service:
|
||||
```powershell
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Troubleshooting
|
||||
|
||||
### Health Checks
|
||||
|
||||
**Service Health**:
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot | Select-Object Name, Status, StartType
|
||||
```
|
||||
|
||||
**API Health**:
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
```
|
||||
|
||||
**Database Stats**:
|
||||
```powershell
|
||||
(Invoke-WebRequest http://localhost:8002/internal/stats).Content | ConvertFrom-Json
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Service Won't Start
|
||||
|
||||
**Symptoms**: Service shows "Stopped" or "Starting" forever
|
||||
|
||||
**Diagnosis**:
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
1. **Missing .env file** → Create from `.env.example`
|
||||
2. **Invalid bot token** → Check `TELEGRAM_BOT_TOKEN`
|
||||
3. **Invalid Claude API key** → Check `CLAUDE_API_KEY`
|
||||
4. **Port 8002 already in use** → Change `INTERNAL_API_PORT`
|
||||
5. **Backend not running** → Start ROA2WEB-Backend service
|
||||
|
||||
**Solutions**:
|
||||
```powershell
|
||||
# Fix config
|
||||
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
|
||||
# Check port availability
|
||||
Get-NetTCPConnection -LocalPort 8002
|
||||
|
||||
# Restart service
|
||||
.\Restart-TelegramBot.ps1
|
||||
```
|
||||
|
||||
#### Bot Not Responding on Telegram
|
||||
|
||||
**Symptoms**: Bot doesn't reply to messages
|
||||
|
||||
**Diagnosis**:
|
||||
1. Check service status:
|
||||
```powershell
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
```
|
||||
2. Check health endpoint:
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
```
|
||||
3. Check logs for errors:
|
||||
```powershell
|
||||
Get-Content logs\stderr.log -Tail 50
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
1. **Service stopped** → Start service
|
||||
2. **Invalid bot token** → Update `.env`
|
||||
3. **Network issues** → Check internet connectivity
|
||||
4. **Bot blocked by user** → User must /start bot again
|
||||
|
||||
#### Account Linking Fails
|
||||
|
||||
**Symptoms**: `/link CODE` returns error
|
||||
|
||||
**Diagnosis**:
|
||||
```powershell
|
||||
# Check internal API
|
||||
Invoke-WebRequest http://localhost:8002/internal/stats
|
||||
|
||||
# Check backend connectivity
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
```
|
||||
|
||||
**Common Causes**:
|
||||
1. **Code expired** (15 min) → Generate new code
|
||||
2. **Backend unreachable** → Check ROA2WEB-Backend service
|
||||
3. **Database error** → Check SQLite file permissions
|
||||
|
||||
#### Database Errors
|
||||
|
||||
**Symptoms**: "Database locked" or "Cannot open database"
|
||||
|
||||
**Diagnosis**:
|
||||
```powershell
|
||||
# Check database file
|
||||
Test-Path C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
|
||||
# Check file permissions
|
||||
icacls C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
```powershell
|
||||
# Stop service
|
||||
.\Stop-TelegramBot.ps1
|
||||
|
||||
# Fix permissions
|
||||
icacls C:\inetpub\wwwroot\roa2web\telegram-bot\data /grant "SYSTEM:(OI)(CI)F" /T
|
||||
|
||||
# Restart service
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Secrets Management
|
||||
|
||||
**NEVER**:
|
||||
- Commit `.env` to git
|
||||
- Share bot token or API keys
|
||||
- Log sensitive data
|
||||
|
||||
**ALWAYS**:
|
||||
- Keep backups of `.env` in secure location
|
||||
- Rotate API keys periodically
|
||||
- Use strong file permissions
|
||||
|
||||
**File Permissions**:
|
||||
```powershell
|
||||
# Restrict .env to SYSTEM and Administrators only
|
||||
icacls C:\inetpub\wwwroot\roa2web\telegram-bot\.env /grant "SYSTEM:F" /grant "Administrators:F" /inheritance:r
|
||||
```
|
||||
|
||||
### Network Security
|
||||
|
||||
- **Internal API (8002)**: Bind to 127.0.0.1 (localhost only)
|
||||
- **Backend API (8000)**: Already on localhost
|
||||
- **No Firewall Rules Needed**: All communication is local
|
||||
|
||||
### Bot Security
|
||||
|
||||
- **Account Linking**: 8-character codes, 15-minute expiry
|
||||
- **JWT Tokens**: Signed and verified by backend
|
||||
- **Rate Limiting**: Built into authentication middleware
|
||||
- **Session Timeout**: 60 minutes of inactivity
|
||||
|
||||
---
|
||||
|
||||
## Maintenance Procedures
|
||||
|
||||
### Updates and Deployments
|
||||
|
||||
1. **Build new deployment package** (dev machine):
|
||||
```bash
|
||||
./Build-TelegramBot.ps1
|
||||
```
|
||||
|
||||
2. **Transfer to server**:
|
||||
```powershell
|
||||
Copy-Item -Path ./deploy-package/telegram-bot -Destination \\10.0.20.36\C$\Temp\telegram-bot-update -Recurse
|
||||
```
|
||||
|
||||
3. **Deploy update** (server):
|
||||
```powershell
|
||||
cd C:\Temp\telegram-bot-update\scripts
|
||||
.\Deploy-TelegramBot.ps1
|
||||
```
|
||||
|
||||
**Deployment Features**:
|
||||
- ✅ Automatic backup before deployment
|
||||
- ✅ Stops service, updates files, restarts service
|
||||
- ✅ Preserves `.env` configuration
|
||||
- ✅ Automatic rollback on failure
|
||||
- ✅ Health check after deployment
|
||||
|
||||
### Log Rotation
|
||||
|
||||
Logs are automatically rotated by Python logging:
|
||||
- **Max size**: 10 MB per file
|
||||
- **Backups**: 5 old log files kept
|
||||
- **Location**: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\`
|
||||
|
||||
**Manual cleanup**:
|
||||
```powershell
|
||||
# Delete old logs (older than 30 days)
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\telegram-bot\logs\*.log |
|
||||
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
|
||||
Remove-Item -Force
|
||||
```
|
||||
|
||||
### Database Maintenance
|
||||
|
||||
**Cleanup expired data** (automatic via scheduled task):
|
||||
- Expired auth codes (older than 15 minutes)
|
||||
- Old sessions (inactive for 60+ minutes)
|
||||
- Runs hourly automatically
|
||||
|
||||
**Manual cleanup**:
|
||||
```sql
|
||||
-- Connect to database
|
||||
sqlite3 C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
|
||||
-- Delete expired codes
|
||||
DELETE FROM telegram_auth_codes WHERE created_at < datetime('now', '-15 minutes');
|
||||
|
||||
-- Delete old sessions
|
||||
DELETE FROM telegram_sessions WHERE updated_at < datetime('now', '-60 minutes');
|
||||
```
|
||||
|
||||
### Service Health Monitoring
|
||||
|
||||
**Daily Checks** (manual or script):
|
||||
```powershell
|
||||
# Service status
|
||||
Get-Service ROA2WEB-TelegramBot
|
||||
|
||||
# Health endpoint
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
|
||||
# Check logs for errors
|
||||
Get-Content logs\stderr.log -Tail 50 | Select-String "ERROR"
|
||||
|
||||
# Check database size
|
||||
(Get-Item data\telegram_bot.db).Length / 1MB
|
||||
```
|
||||
|
||||
**Weekly Checks**:
|
||||
- Review backup logs
|
||||
- Check backup retention (30 days)
|
||||
- Review disk space usage
|
||||
- Check for Windows updates
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### PowerShell Scripts Reference
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `Install-TelegramBot.ps1` | Initial installation |
|
||||
| `Deploy-TelegramBot.ps1` | Deploy updates |
|
||||
| `Build-TelegramBot.ps1` | Build deployment package |
|
||||
| `Start-TelegramBot.ps1` | Start service |
|
||||
| `Stop-TelegramBot.ps1` | Stop service |
|
||||
| `Restart-TelegramBot.ps1` | Restart service |
|
||||
| `Backup-TelegramDB.ps1` | Manual database backup |
|
||||
| `Setup-DailyBackup.ps1` | Configure automated backups |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/internal/health` | GET | Health check |
|
||||
| `/internal/stats` | GET | Database statistics |
|
||||
| `/internal/save-code` | POST | Save auth code (from backend) |
|
||||
| `/internal/verify-code` | POST | Verify auth code |
|
||||
|
||||
### Database Schema
|
||||
|
||||
**telegram_users**:
|
||||
```sql
|
||||
CREATE TABLE telegram_users (
|
||||
telegram_user_id INTEGER PRIMARY KEY,
|
||||
oracle_user_id INTEGER NOT NULL,
|
||||
oracle_username TEXT NOT NULL,
|
||||
jwt_token TEXT NOT NULL,
|
||||
jwt_refresh_token TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**telegram_auth_codes**:
|
||||
```sql
|
||||
CREATE TABLE telegram_auth_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
oracle_user_id INTEGER NOT NULL,
|
||||
oracle_username TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used INTEGER DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
**telegram_sessions**:
|
||||
```sql
|
||||
CREATE TABLE telegram_sessions (
|
||||
telegram_user_id INTEGER PRIMARY KEY,
|
||||
conversation_history TEXT, -- JSON
|
||||
session_data TEXT, -- JSON
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
**Documentation**:
|
||||
- Project README: `/mnt/e/proiecte/roa2web/roa2web/reports-app/telegram-bot/README.md`
|
||||
- Progress Tracker: `/mnt/e/proiecte/roa2web/roa2web/development/TELEGRAM_BOT_PROGRESS.md`
|
||||
- Production Deployment Plan: `/mnt/e/proiecte/roa2web/roa2web/development/TELEGRAM_BOT_PRODUCTION_DEPLOYMENT.md`
|
||||
|
||||
**Logs Location**:
|
||||
- Service Output: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log`
|
||||
- Service Errors: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log`
|
||||
- Backups: `C:\inetpub\wwwroot\roa2web\telegram-bot\logs\backup.log`
|
||||
|
||||
**Contact**: ROA2WEB Development Team
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2025-10-22
|
||||
**Status**: Production Ready
|
||||
917
deployment/windows/docs/WINDOWS_DEPLOYMENT.md
Normal file
917
deployment/windows/docs/WINDOWS_DEPLOYMENT.md
Normal file
@@ -0,0 +1,917 @@
|
||||
# ROA2WEB - Windows Server Deployment Guide
|
||||
|
||||
Complete deployment guide for ROA2WEB on Windows Server with IIS and Oracle Database.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Architecture](#architecture)
|
||||
4. [Initial Setup](#initial-setup)
|
||||
5. [Deployment Workflow](#deployment-workflow)
|
||||
6. [Configuration](#configuration)
|
||||
7. [Management](#management)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
9. [Maintenance](#maintenance)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This guide provides step-by-step instructions for deploying ROA2WEB on Windows Server with:
|
||||
|
||||
- **Backend**: FastAPI as Windows Service (port 8000)
|
||||
- **Frontend**: Vue.js static files served by IIS (port 80/443)
|
||||
- **Database**: Direct connection to local Oracle DB (no SSH tunnel)
|
||||
- **Reverse Proxy**: IIS with URL Rewrite for API routing
|
||||
|
||||
### Key Features
|
||||
|
||||
✅ Simple installation with PowerShell scripts
|
||||
✅ Minimal dependencies (Python + IIS)
|
||||
✅ Easy replication across multiple servers
|
||||
✅ Windows Service for backend (auto-start, auto-restart)
|
||||
✅ Production-ready configuration
|
||||
|
||||
---
|
||||
|
||||
## 📦 Prerequisites
|
||||
|
||||
### Server Requirements
|
||||
|
||||
| Component | Requirement | Notes |
|
||||
|-----------|-------------|-------|
|
||||
| **OS** | Windows Server 2016+ | Or Windows 10/11 Pro |
|
||||
| **RAM** | 4GB minimum | 8GB recommended |
|
||||
| **Disk** | 10GB free space | For application and logs |
|
||||
| **CPU** | 2 cores minimum | 4 cores recommended |
|
||||
|
||||
### Software Requirements
|
||||
|
||||
#### Required (will be installed automatically)
|
||||
|
||||
- **IIS** (Internet Information Services)
|
||||
- **Python 3.11+**
|
||||
- **NSSM** (Non-Sucking Service Manager)
|
||||
- **IIS URL Rewrite Module**
|
||||
- **IIS Application Request Routing (ARR)**
|
||||
|
||||
#### Pre-installed
|
||||
|
||||
- **Oracle Database** (local or network-accessible)
|
||||
- **Oracle Instant Client** (for Python oracledb)
|
||||
|
||||
#### On Development Machine
|
||||
|
||||
- **Node.js 16+** (for building frontend)
|
||||
- **Git** (optional, for cloning repository)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Deployment Structure
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\
|
||||
├── backend\ # FastAPI application
|
||||
│ ├── app\ # Application code
|
||||
│ ├── requirements.txt # Python dependencies
|
||||
│ ├── .env # Environment configuration
|
||||
│ └── logs\ # Application logs
|
||||
│
|
||||
├── frontend\ # Vue.js static files
|
||||
│ ├── index.html
|
||||
│ ├── assets\
|
||||
│ ├── web.config # IIS configuration
|
||||
│ └── ...
|
||||
│
|
||||
├── logs\ # Service logs
|
||||
│ ├── backend-stdout.log
|
||||
│ └── backend-stderr.log
|
||||
│
|
||||
├── temp\ # Temporary files
|
||||
│
|
||||
└── backups\ # Deployment backups
|
||||
└── backup-YYYYMMDD-HHMMSS\
|
||||
```
|
||||
|
||||
### Network Flow
|
||||
|
||||
```
|
||||
Client Browser
|
||||
↓
|
||||
IIS (Port 80/443)
|
||||
↓
|
||||
├─→ /api/* ────→ Backend Service (localhost:8000)
|
||||
│ ↓
|
||||
│ Oracle Database (localhost:1521)
|
||||
│
|
||||
└─→ /* ─────────→ Frontend Static Files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Initial Setup
|
||||
|
||||
### Step 1: Install IIS
|
||||
|
||||
Open PowerShell as Administrator:
|
||||
|
||||
```powershell
|
||||
# Install IIS with required features
|
||||
Install-WindowsFeature -Name Web-Server -IncludeManagementTools
|
||||
|
||||
# Verify installation
|
||||
Get-WindowsFeature -Name Web-Server
|
||||
```
|
||||
|
||||
### Step 2: Prepare Deployment Package
|
||||
|
||||
**On your development machine (WSL/Windows):**
|
||||
|
||||
```bash
|
||||
# Navigate to deployment scripts
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/deployment/windows/scripts
|
||||
|
||||
# Build frontend and create deployment package
|
||||
./Build-Frontend.ps1
|
||||
|
||||
# Output will be in: ./deploy-package
|
||||
```
|
||||
|
||||
This creates a complete deployment package:
|
||||
```
|
||||
deploy-package/
|
||||
├── backend/ # Backend files
|
||||
├── frontend/ # Built Vue.js files
|
||||
├── config/ # Configuration templates
|
||||
└── README.txt # Deployment instructions
|
||||
```
|
||||
|
||||
### Step 3: Transfer to Server
|
||||
|
||||
**Option A: Network Share**
|
||||
```powershell
|
||||
# On development machine
|
||||
Copy-Item -Path .\deploy-package -Destination \\SERVER-IP\C$\Temp\roa2web -Recurse
|
||||
```
|
||||
|
||||
**Option B: Manual Transfer**
|
||||
- Zip the `deploy-package` folder
|
||||
- Transfer via RDP, FTP, or USB
|
||||
- Extract on server to `C:\Temp\roa2web`
|
||||
|
||||
### Step 4: Run Installation Script
|
||||
|
||||
**On Windows Server (PowerShell as Administrator):**
|
||||
|
||||
```powershell
|
||||
# Navigate to deployment scripts
|
||||
cd C:\path\to\roa2web\deployment\windows\scripts
|
||||
|
||||
# Run installation
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
# Installation will:
|
||||
# - Install Python, NSSM, IIS modules
|
||||
# - Create directory structure
|
||||
# - Install Python dependencies
|
||||
# - Create Windows Service
|
||||
# - Configure IIS website
|
||||
```
|
||||
|
||||
**Installation Parameters:**
|
||||
|
||||
```powershell
|
||||
# Custom installation path
|
||||
.\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web"
|
||||
|
||||
# Custom service port
|
||||
.\Install-ROA2WEB.ps1 -ServicePort 8001
|
||||
|
||||
# Skip Python installation (if already installed)
|
||||
.\Install-ROA2WEB.ps1 -SkipPython
|
||||
|
||||
# Skip IIS configuration
|
||||
.\Install-ROA2WEB.ps1 -SkipIIS
|
||||
```
|
||||
|
||||
### Step 5: Configure Application
|
||||
|
||||
**Edit configuration file:**
|
||||
|
||||
```powershell
|
||||
# Copy environment template
|
||||
Copy-Item C:\inetpub\wwwroot\roa2web\backend\config\.env.production.windows `
|
||||
C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
# Edit with your values
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
```
|
||||
|
||||
**Required configuration:**
|
||||
|
||||
```env
|
||||
# Oracle Database
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
# Database password - configure in .env
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# JWT Secret (generate new one!)
|
||||
JWT_SECRET_KEY=GENERATE_STRONG_RANDOM_STRING_HERE
|
||||
```
|
||||
|
||||
**Generate JWT Secret:**
|
||||
|
||||
```powershell
|
||||
# PowerShell method
|
||||
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | % {[char]$_})
|
||||
|
||||
# Or use online tool: https://generate-secret.vercel.app/
|
||||
```
|
||||
|
||||
### Step 6: Start Services
|
||||
|
||||
```powershell
|
||||
# Start backend service
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
# Check service status
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Check logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50
|
||||
```
|
||||
|
||||
### Step 7: Verify Installation
|
||||
|
||||
**Test endpoints:**
|
||||
|
||||
```powershell
|
||||
# Backend health check
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/health"
|
||||
|
||||
# API documentation
|
||||
Start-Process "http://localhost:8000/docs"
|
||||
|
||||
# Frontend application
|
||||
Start-Process "http://localhost"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Deployment Workflow
|
||||
|
||||
### For Updates and New Deployments
|
||||
|
||||
**1. Build on Development Machine:**
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/deployment/windows/scripts
|
||||
./Build-Frontend.ps1 -OutputPath "./deploy-$(date +%Y%m%d)"
|
||||
```
|
||||
|
||||
**2. Transfer to Server:**
|
||||
|
||||
```powershell
|
||||
# Copy deployment package to server
|
||||
Copy-Item -Path .\deploy-20250118 -Destination C:\Temp\roa2web-deploy -Recurse
|
||||
```
|
||||
|
||||
**3. Deploy on Server:**
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\deployment\windows\scripts
|
||||
|
||||
# Run deployment script
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\Temp\roa2web-deploy"
|
||||
|
||||
# The script will:
|
||||
# - Create backup of current deployment
|
||||
# - Stop backend service
|
||||
# - Update backend and frontend files
|
||||
# - Install new Python dependencies (if changed)
|
||||
# - Restart backend service
|
||||
# - Validate deployment health
|
||||
```
|
||||
|
||||
**Deployment Options:**
|
||||
|
||||
```powershell
|
||||
# Update only backend
|
||||
.\Deploy-ROA2WEB.ps1 -UpdateFrontend $false
|
||||
|
||||
# Update only frontend
|
||||
.\Deploy-ROA2WEB.ps1 -UpdateBackend $false
|
||||
|
||||
# Skip backup (not recommended)
|
||||
.\Deploy-ROA2WEB.ps1 -BackupEnabled $false
|
||||
|
||||
# Skip service restart
|
||||
.\Deploy-ROA2WEB.ps1 -RestartService $false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
### Backend Configuration (.env)
|
||||
|
||||
**Location:** `C:\inetpub\wwwroot\roa2web\backend\.env`
|
||||
|
||||
**Essential settings:**
|
||||
|
||||
```env
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
DEBUG=false
|
||||
|
||||
# Oracle Database
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
# Database password - configure in .env
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# Connection Pool
|
||||
ORACLE_MIN_POOL_SIZE=2
|
||||
ORACLE_MAX_POOL_SIZE=10
|
||||
|
||||
# JWT Authentication
|
||||
JWT_SECRET_KEY=your_strong_secret_key
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRE_MINUTES=480
|
||||
|
||||
# Server Settings
|
||||
HOST=127.0.0.1
|
||||
PORT=8000
|
||||
WORKERS=4
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=C:\inetpub\wwwroot\roa2web\backend\logs\app.log
|
||||
```
|
||||
|
||||
### IIS Configuration (web.config)
|
||||
|
||||
**Location:** `C:\inetpub\wwwroot\roa2web\frontend\web.config`
|
||||
|
||||
This file is automatically created during installation. Key features:
|
||||
|
||||
- **SPA Routing**: All non-file requests fallback to `index.html`
|
||||
- **API Reverse Proxy**: `/api/*` routed to backend service
|
||||
- **Compression**: Gzip compression enabled
|
||||
- **Caching**: Static assets cached for 1 year
|
||||
- **Security Headers**: X-Frame-Options, CSP, HSTS
|
||||
|
||||
**No manual configuration needed** - works out of the box!
|
||||
|
||||
### Windows Service Configuration
|
||||
|
||||
**Service Name:** `ROA2WEB-Backend`
|
||||
**Startup Type:** Automatic
|
||||
**Recovery:** Restart on failure (5 second delay)
|
||||
|
||||
**View/Edit service:**
|
||||
|
||||
```powershell
|
||||
# Service properties
|
||||
Get-Service ROA2WEB-Backend | Format-List *
|
||||
|
||||
# Service configuration
|
||||
sc.exe qc ROA2WEB-Backend
|
||||
|
||||
# Modify with NSSM GUI
|
||||
nssm edit ROA2WEB-Backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Management
|
||||
|
||||
### Service Management
|
||||
|
||||
**PowerShell Scripts:**
|
||||
|
||||
```powershell
|
||||
# Start service
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
# Stop service
|
||||
.\Stop-ROA2WEB.ps1
|
||||
|
||||
# Restart service
|
||||
.\Restart-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
**Manual Service Management:**
|
||||
|
||||
```powershell
|
||||
# Start
|
||||
Start-Service ROA2WEB-Backend
|
||||
|
||||
# Stop
|
||||
Stop-Service ROA2WEB-Backend
|
||||
|
||||
# Restart
|
||||
Restart-Service ROA2WEB-Backend
|
||||
|
||||
# Status
|
||||
Get-Service ROA2WEB-Backend
|
||||
```
|
||||
|
||||
**Windows Services GUI:**
|
||||
|
||||
```powershell
|
||||
services.msc
|
||||
# Find: ROA2WEB Backend Service
|
||||
```
|
||||
|
||||
### Log Management
|
||||
|
||||
**Log Locations:**
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\logs\
|
||||
├── backend-stdout.log # Service output
|
||||
├── backend-stderr.log # Service errors
|
||||
└── app.log # Application log
|
||||
```
|
||||
|
||||
**View Logs:**
|
||||
|
||||
```powershell
|
||||
# Real-time monitoring
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
|
||||
# Last 100 lines
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 100
|
||||
|
||||
# Search for errors
|
||||
Select-String -Path "C:\inetpub\wwwroot\roa2web\logs\*.log" -Pattern "ERROR|CRITICAL"
|
||||
|
||||
# Filter by date
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\app.log |
|
||||
Select-String -Pattern "2025-01-18"
|
||||
```
|
||||
|
||||
**Log Rotation:**
|
||||
|
||||
Logs are automatically rotated when they reach 10MB (configured in .env):
|
||||
|
||||
```env
|
||||
LOG_MAX_SIZE=10485760 # 10 MB
|
||||
LOG_BACKUP_COUNT=5 # Keep 5 old logs
|
||||
```
|
||||
|
||||
### IIS Management
|
||||
|
||||
**PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Website status
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# Start/Stop website
|
||||
Start-Website ROA2WEB
|
||||
Stop-Website ROA2WEB
|
||||
|
||||
# Application pool
|
||||
Get-WebAppPoolState ROA2WEB-AppPool
|
||||
Restart-WebAppPool ROA2WEB-AppPool
|
||||
|
||||
# View configuration
|
||||
Get-WebConfiguration -Filter "system.webServer/rewrite/rules"
|
||||
```
|
||||
|
||||
**IIS Manager GUI:**
|
||||
|
||||
```powershell
|
||||
inetmgr
|
||||
# Navigate to: Sites → ROA2WEB
|
||||
```
|
||||
|
||||
### Backup Management
|
||||
|
||||
**Deployment backups are automatic!**
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\backups\
|
||||
├── backup-20250118-103045\
|
||||
├── backup-20250118-154512\
|
||||
└── backup-20250117-090123\
|
||||
```
|
||||
|
||||
Last 10 backups are kept automatically.
|
||||
|
||||
**Manual Backup:**
|
||||
|
||||
```powershell
|
||||
# Create backup
|
||||
$date = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
Copy-Item -Path C:\inetpub\wwwroot\roa2web `
|
||||
-Destination C:\Backups\roa2web-$date `
|
||||
-Recurse -Exclude logs,temp,backups
|
||||
```
|
||||
|
||||
**Restore from Backup:**
|
||||
|
||||
```powershell
|
||||
# Stop service
|
||||
.\Stop-ROA2WEB.ps1
|
||||
|
||||
# Restore files
|
||||
Copy-Item -Path C:\inetpub\wwwroot\roa2web\backups\backup-20250118-103045\* `
|
||||
-Destination C:\inetpub\wwwroot\roa2web `
|
||||
-Recurse -Force
|
||||
|
||||
# Start service
|
||||
.\Start-ROA2WEB.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Service Won't Start
|
||||
|
||||
**Symptom:** Backend service fails to start or stops immediately.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# View error log
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 50
|
||||
|
||||
# Test Python manually
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
1. **Python not found:**
|
||||
```powershell
|
||||
# Check Python installation
|
||||
python --version
|
||||
# Add to PATH if needed
|
||||
```
|
||||
|
||||
2. **Module import errors:**
|
||||
```powershell
|
||||
# Reinstall dependencies
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Oracle connection failed:**
|
||||
```powershell
|
||||
# Check Oracle listener
|
||||
lsnrctl status
|
||||
# Test connection
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
```
|
||||
|
||||
4. **Port already in use:**
|
||||
```powershell
|
||||
# Check what's using port 8000
|
||||
netstat -ano | findstr :8000
|
||||
# Kill process or change port in .env
|
||||
```
|
||||
|
||||
### Frontend Not Loading
|
||||
|
||||
**Symptom:** Blank page or 404 errors.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# IIS website running?
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# Files exist?
|
||||
Test-Path C:\inetpub\wwwroot\roa2web\frontend\index.html
|
||||
|
||||
# Check IIS logs
|
||||
Get-Content C:\inetpub\logs\LogFiles\W3SVC*\*.log -Tail 50
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```powershell
|
||||
# Restart IIS site
|
||||
Stop-Website ROA2WEB
|
||||
Start-Website ROA2WEB
|
||||
|
||||
# Restart app pool
|
||||
Restart-WebAppPool ROA2WEB-AppPool
|
||||
|
||||
# Verify web.config
|
||||
Test-Path C:\inetpub\wwwroot\roa2web\frontend\web.config
|
||||
```
|
||||
|
||||
### API Calls Failing (502/504 errors)
|
||||
|
||||
**Symptom:** Frontend loads but API calls fail.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Backend service running?
|
||||
Get-Service ROA2WEB-Backend
|
||||
|
||||
# Backend responding?
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/health"
|
||||
|
||||
# Check ARR proxy
|
||||
Get-WebConfiguration -Filter "system.webServer/proxy"
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Enable ARR proxy:**
|
||||
```powershell
|
||||
Set-WebConfigurationProperty -PSPath "MACHINE/WEBROOT/APPHOST" `
|
||||
-Filter "system.webServer/proxy" `
|
||||
-Name "enabled" `
|
||||
-Value "True"
|
||||
```
|
||||
|
||||
2. **Check backend logs:**
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 100
|
||||
```
|
||||
|
||||
3. **Test backend directly:**
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/api/health"
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
**Symptom:** Backend starts but database queries fail.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Oracle client installed?
|
||||
dir $env:ORACLE_HOME
|
||||
|
||||
# TNS names configured?
|
||||
$env:TNS_ADMIN
|
||||
Get-Content $env:TNS_ADMIN\tnsnames.ora
|
||||
|
||||
# Test connection
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Install Oracle Instant Client:**
|
||||
- Download from: https://www.oracle.com/database/technologies/instant-client/downloads.html
|
||||
- Extract to C:\oracle\instantclient_19_x
|
||||
- Add to PATH
|
||||
|
||||
2. **Configure .env:**
|
||||
```env
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
```
|
||||
|
||||
3. **Check Oracle service:**
|
||||
```powershell
|
||||
Get-Service Oracle*
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
**Symptom:** Access denied errors.
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Check folder permissions
|
||||
icacls C:\inetpub\wwwroot\roa2web
|
||||
|
||||
# Check service account
|
||||
sc.exe qc ROA2WEB-Backend
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```powershell
|
||||
# Grant IIS user read access
|
||||
icacls C:\inetpub\wwwroot\roa2web /grant IIS_IUSRS:(OI)(CI)RX
|
||||
|
||||
# Grant service account full access to backend
|
||||
icacls C:\inetpub\wwwroot\roa2web\backend /grant "NT AUTHORITY\LOCAL SERVICE":(OI)(CI)F
|
||||
```
|
||||
|
||||
### High CPU/Memory Usage
|
||||
|
||||
**Check:**
|
||||
|
||||
```powershell
|
||||
# Service resource usage
|
||||
Get-Process -Name python | Format-Table ProcessName, CPU, WS
|
||||
|
||||
# Check worker count
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\backend\.env | Select-String WORKERS
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
```env
|
||||
# Reduce workers in .env
|
||||
WORKERS=2
|
||||
|
||||
# Reduce pool size
|
||||
ORACLE_MAX_POOL_SIZE=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Maintenance
|
||||
|
||||
### Regular Maintenance Tasks
|
||||
|
||||
**Daily:**
|
||||
- Check service status
|
||||
- Monitor disk space
|
||||
- Review error logs
|
||||
|
||||
```powershell
|
||||
# Daily check script
|
||||
Get-Service ROA2WEB-Backend
|
||||
Get-PSDrive C | Select-Object Used,Free
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log -Tail 20
|
||||
```
|
||||
|
||||
**Weekly:**
|
||||
- Clean old logs
|
||||
- Verify backups
|
||||
- Update dependencies (if needed)
|
||||
|
||||
```powershell
|
||||
# Clean logs older than 30 days
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\logs\*.log.* |
|
||||
Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-30)} |
|
||||
Remove-Item
|
||||
|
||||
# List backups
|
||||
Get-ChildItem C:\inetpub\wwwroot\roa2web\backups
|
||||
```
|
||||
|
||||
**Monthly:**
|
||||
- Review security updates
|
||||
- Performance optimization
|
||||
- Database maintenance
|
||||
|
||||
### Updating Python Dependencies
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\backend
|
||||
|
||||
# Update all packages
|
||||
pip install --upgrade -r requirements.txt
|
||||
|
||||
# Restart service
|
||||
Restart-Service ROA2WEB-Backend
|
||||
```
|
||||
|
||||
### Database Maintenance
|
||||
|
||||
```sql
|
||||
-- Connect to Oracle
|
||||
sqlplus CONTAFIN_ORACLE/password@localhost:1521/ROA
|
||||
|
||||
-- Check table statistics
|
||||
SELECT table_name, num_rows, last_analyzed
|
||||
FROM user_tables
|
||||
ORDER BY last_analyzed;
|
||||
|
||||
-- Update statistics
|
||||
EXEC DBMS_STATS.gather_schema_stats('CONTAFIN_ORACLE');
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
**Built-in health check:**
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:8000/health" |
|
||||
Select-Object StatusCode, Content
|
||||
```
|
||||
|
||||
**Windows Performance Monitor:**
|
||||
|
||||
```powershell
|
||||
perfmon
|
||||
# Add counters:
|
||||
# - Process > % Processor Time > python.exe
|
||||
# - Process > Private Bytes > python.exe
|
||||
# - Web Service > Current Connections
|
||||
```
|
||||
|
||||
### Security Updates
|
||||
|
||||
**Windows Updates:**
|
||||
|
||||
```powershell
|
||||
# Check for updates
|
||||
Get-WindowsUpdate
|
||||
|
||||
# Install updates
|
||||
Install-WindowsUpdate -AcceptAll
|
||||
```
|
||||
|
||||
**Python Security Updates:**
|
||||
|
||||
```powershell
|
||||
# Check for vulnerabilities
|
||||
pip check
|
||||
|
||||
# Update specific package
|
||||
pip install --upgrade fastapi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
### Documentation Files
|
||||
|
||||
- `config/.env.production.windows` - Configuration template
|
||||
- `config/web.config` - IIS configuration
|
||||
- `scripts/*.ps1` - PowerShell scripts
|
||||
|
||||
### PowerShell Scripts Reference
|
||||
|
||||
| Script | Purpose | Usage |
|
||||
|--------|---------|-------|
|
||||
| `Install-ROA2WEB.ps1` | Initial installation | `.\Install-ROA2WEB.ps1` |
|
||||
| `Deploy-ROA2WEB.ps1` | Deploy updates | `.\Deploy-ROA2WEB.ps1 -SourcePath <path>` |
|
||||
| `Build-Frontend.ps1` | Build Vue.js frontend | `.\Build-Frontend.ps1` |
|
||||
| `Start-ROA2WEB.ps1` | Start backend service | `.\Start-ROA2WEB.ps1` |
|
||||
| `Stop-ROA2WEB.ps1` | Stop backend service | `.\Stop-ROA2WEB.ps1` |
|
||||
| `Restart-ROA2WEB.ps1` | Restart backend service | `.\Restart-ROA2WEB.ps1` |
|
||||
|
||||
### Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check logs: `C:\inetpub\wwwroot\roa2web\logs\`
|
||||
2. Review this documentation
|
||||
3. Contact: development-team@your-company.com
|
||||
|
||||
---
|
||||
|
||||
## ✅ Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```powershell
|
||||
# Service management
|
||||
.\Start-ROA2WEB.ps1
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Restart-ROA2WEB.ps1
|
||||
|
||||
# Check status
|
||||
Get-Service ROA2WEB-Backend
|
||||
Get-Website ROA2WEB
|
||||
|
||||
# View logs
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\logs\backend-stdout.log -Tail 50 -Wait
|
||||
|
||||
# Health check
|
||||
Invoke-WebRequest http://localhost:8000/health
|
||||
|
||||
# Deploy update
|
||||
.\Deploy-ROA2WEB.ps1 -SourcePath "C:\Temp\roa2web-deploy"
|
||||
```
|
||||
|
||||
### Key Locations
|
||||
|
||||
- **Application**: `C:\inetpub\wwwroot\roa2web\`
|
||||
- **Backend**: `C:\inetpub\wwwroot\roa2web\backend\`
|
||||
- **Frontend**: `C:\inetpub\wwwroot\roa2web\frontend\`
|
||||
- **Logs**: `C:\inetpub\wwwroot\roa2web\logs\`
|
||||
- **Config**: `C:\inetpub\wwwroot\roa2web\backend\.env`
|
||||
- **Backups**: `C:\inetpub\wwwroot\roa2web\backups\`
|
||||
|
||||
### Access Points
|
||||
|
||||
- **Web App**: http://localhost or http://server-ip
|
||||
- **API Docs**: http://localhost:8000/docs
|
||||
- **Health Check**: http://localhost:8000/health
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-01-18*
|
||||
*Version: 2.0.0*
|
||||
*ROA2WEB Windows Deployment Guide*
|
||||
361
deployment/windows/scripts/Backup-TelegramDB.ps1
Normal file
361
deployment/windows/scripts/Backup-TelegramDB.ps1
Normal file
@@ -0,0 +1,361 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Backup ROA2WEB Telegram Bot SQLite Database
|
||||
|
||||
.DESCRIPTION
|
||||
This script creates a backup of the Telegram bot's SQLite database:
|
||||
- Copies database file with timestamp
|
||||
- Compresses backup (optional)
|
||||
- Cleans up old backups (keeps last 30 days)
|
||||
- Logs backup operations
|
||||
- Can run manually or via scheduled task
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER RetentionDays
|
||||
Number of days to keep backups (default: 30)
|
||||
|
||||
.PARAMETER Compress
|
||||
Compress backup file (default: true)
|
||||
|
||||
.PARAMETER LogToFile
|
||||
Write backup log to file (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
.\Backup-TelegramDB.ps1
|
||||
Standard backup with defaults
|
||||
|
||||
.EXAMPLE
|
||||
.\Backup-TelegramDB.ps1 -RetentionDays 60 -Compress $true
|
||||
Backup with 60-day retention and compression
|
||||
|
||||
.EXAMPLE
|
||||
.\Backup-TelegramDB.ps1 -LogToFile $false
|
||||
Backup without logging to file (console only)
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+
|
||||
Can be run manually or via Task Scheduler
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[int]$RetentionDays = 30,
|
||||
[bool]$Compress = $true,
|
||||
[bool]$LogToFile = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
$script:Config = @{
|
||||
InstallPath = $InstallPath
|
||||
DataPath = Join-Path $InstallPath "data"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
DatabaseFile = "telegram_bot.db"
|
||||
RetentionDays = $RetentionDays
|
||||
Compress = $Compress
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$Level = "INFO"
|
||||
)
|
||||
|
||||
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$logMessage = "[$timestamp] [$Level] $Message"
|
||||
|
||||
# Console output
|
||||
switch ($Level) {
|
||||
"ERROR" { Write-Host $logMessage -ForegroundColor Red }
|
||||
"WARN" { Write-Host $logMessage -ForegroundColor Yellow }
|
||||
"SUCCESS" { Write-Host $logMessage -ForegroundColor Green }
|
||||
default { Write-Host $logMessage -ForegroundColor Cyan }
|
||||
}
|
||||
|
||||
# File output
|
||||
if ($LogToFile) {
|
||||
$logFile = Join-Path $Config.LogsPath "backup.log"
|
||||
Add-Content -Path $logFile -Value $logMessage -Encoding UTF8
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DatabaseFile {
|
||||
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
|
||||
|
||||
if (-not (Test-Path $dbPath)) {
|
||||
Write-Log "Database file not found: $dbPath" -Level "ERROR"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Check if file is accessible (not locked)
|
||||
try {
|
||||
$stream = [System.IO.File]::Open($dbPath, 'Open', 'Read', 'Read')
|
||||
$stream.Close()
|
||||
Write-Log "Database file is accessible: $dbPath" -Level "INFO"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Log "Database file is locked or inaccessible: $_" -Level "ERROR"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DatabaseSize {
|
||||
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
|
||||
$size = (Get-Item $dbPath).Length / 1KB
|
||||
return [math]::Round($size, 2)
|
||||
}
|
||||
|
||||
function New-BackupDirectory {
|
||||
if (-not (Test-Path $Config.BackupPath)) {
|
||||
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
|
||||
Write-Log "Created backup directory: $($Config.BackupPath)" -Level "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
function Backup-Database {
|
||||
Write-Log "Starting database backup..." -Level "INFO"
|
||||
|
||||
# Ensure backup directory exists
|
||||
New-BackupDirectory
|
||||
|
||||
# Generate backup filename with timestamp
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupFileName = "telegram_bot_backup_$timestamp.db"
|
||||
$backupFilePath = Join-Path $Config.BackupPath $backupFileName
|
||||
|
||||
# Source database path
|
||||
$dbPath = Join-Path $Config.DataPath $Config.DatabaseFile
|
||||
|
||||
try {
|
||||
# Copy database file
|
||||
Copy-Item -Path $dbPath -Destination $backupFilePath -Force
|
||||
Write-Log "Database backed up to: $backupFileName" -Level "SUCCESS"
|
||||
|
||||
# Get backup file size
|
||||
$backupSize = (Get-Item $backupFilePath).Length / 1KB
|
||||
Write-Log "Backup size: $([math]::Round($backupSize, 2)) KB" -Level "INFO"
|
||||
|
||||
# Compress if enabled
|
||||
if ($Config.Compress) {
|
||||
$compressedPath = Compress-Backup -BackupPath $backupFilePath
|
||||
if ($compressedPath) {
|
||||
# Remove uncompressed backup
|
||||
Remove-Item -Path $backupFilePath -Force
|
||||
Write-Log "Uncompressed backup removed" -Level "INFO"
|
||||
return $compressedPath
|
||||
}
|
||||
}
|
||||
|
||||
return $backupFilePath
|
||||
} catch {
|
||||
Write-Log "Backup failed: $_" -Level "ERROR"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Compress-Backup {
|
||||
param([string]$BackupPath)
|
||||
|
||||
Write-Log "Compressing backup..." -Level "INFO"
|
||||
|
||||
$zipPath = "$BackupPath.zip"
|
||||
|
||||
try {
|
||||
# Create ZIP archive
|
||||
Compress-Archive -Path $BackupPath -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||
|
||||
# Get compressed size
|
||||
$originalSize = (Get-Item $BackupPath).Length / 1KB
|
||||
$compressedSize = (Get-Item $zipPath).Length / 1KB
|
||||
$ratio = [math]::Round((1 - ($compressedSize / $originalSize)) * 100, 1)
|
||||
|
||||
Write-Log "Backup compressed: $([math]::Round($compressedSize, 2)) KB (saved $ratio%)" -Level "SUCCESS"
|
||||
|
||||
return $zipPath
|
||||
} catch {
|
||||
Write-Log "Compression failed: $_" -Level "WARN"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-OldBackups {
|
||||
Write-Log "Cleaning up old backups (keeping last $($Config.RetentionDays) days)..." -Level "INFO"
|
||||
|
||||
$cutoffDate = (Get-Date).AddDays(-$Config.RetentionDays)
|
||||
|
||||
try {
|
||||
# Get all backup files (both .db and .zip)
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -File |
|
||||
Where-Object {
|
||||
$_.Name -like "telegram_bot_backup_*.db" -or
|
||||
$_.Name -like "telegram_bot_backup_*.db.zip"
|
||||
}
|
||||
|
||||
$removedCount = 0
|
||||
$freedSpace = 0
|
||||
|
||||
foreach ($backup in $allBackups) {
|
||||
if ($backup.LastWriteTime -lt $cutoffDate) {
|
||||
$size = $backup.Length / 1MB
|
||||
Remove-Item -Path $backup.FullName -Force
|
||||
$removedCount++
|
||||
$freedSpace += $size
|
||||
Write-Log "Removed old backup: $($backup.Name)" -Level "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
if ($removedCount -gt 0) {
|
||||
Write-Log "Removed $removedCount old backup(s), freed $([math]::Round($freedSpace, 2)) MB" -Level "SUCCESS"
|
||||
} else {
|
||||
Write-Log "No old backups to remove" -Level "INFO"
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Failed to clean up old backups: $_" -Level "WARN"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-BackupStatistics {
|
||||
Write-Log "Backup Statistics:" -Level "INFO"
|
||||
|
||||
# Count backups
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -File |
|
||||
Where-Object {
|
||||
$_.Name -like "telegram_bot_backup_*.db" -or
|
||||
$_.Name -like "telegram_bot_backup_*.db.zip"
|
||||
}
|
||||
|
||||
$totalBackups = $allBackups.Count
|
||||
$totalSize = ($allBackups | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
|
||||
Write-Log " Total backups: $totalBackups" -Level "INFO"
|
||||
Write-Log " Total size: $([math]::Round($totalSize, 2)) MB" -Level "INFO"
|
||||
|
||||
# Latest backup
|
||||
if ($totalBackups -gt 0) {
|
||||
$latest = $allBackups | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
Write-Log " Latest backup: $($latest.Name) ($([math]::Round($latest.Length / 1KB, 2)) KB)" -Level "INFO"
|
||||
Write-Log " Created: $($latest.LastWriteTime)" -Level "INFO"
|
||||
}
|
||||
|
||||
# Oldest backup
|
||||
if ($totalBackups -gt 1) {
|
||||
$oldest = $allBackups | Sort-Object LastWriteTime | Select-Object -First 1
|
||||
$age = (Get-Date) - $oldest.LastWriteTime
|
||||
Write-Log " Oldest backup: $($oldest.Name) (Age: $([math]::Round($age.TotalDays, 1)) days)" -Level "INFO"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-BackupIntegrity {
|
||||
param([string]$BackupPath)
|
||||
|
||||
Write-Log "Testing backup integrity..." -Level "INFO"
|
||||
|
||||
try {
|
||||
# For ZIP files, test archive
|
||||
if ($BackupPath -like "*.zip") {
|
||||
# Try to extract to temp location
|
||||
$tempExtract = Join-Path $env:TEMP "telegram_bot_backup_test"
|
||||
if (Test-Path $tempExtract) {
|
||||
Remove-Item -Path $tempExtract -Recurse -Force
|
||||
}
|
||||
|
||||
Expand-Archive -Path $BackupPath -DestinationPath $tempExtract -Force
|
||||
|
||||
# Check if database file exists in extracted folder
|
||||
$extractedDb = Get-ChildItem -Path $tempExtract -Filter "*.db" -Recurse
|
||||
if ($extractedDb) {
|
||||
Write-Log "Backup integrity test PASSED (ZIP archive valid)" -Level "SUCCESS"
|
||||
Remove-Item -Path $tempExtract -Recurse -Force
|
||||
return $true
|
||||
} else {
|
||||
Write-Log "Backup integrity test FAILED (No database file in archive)" -Level "ERROR"
|
||||
Remove-Item -Path $tempExtract -Recurse -Force
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
# For .db files, try to open
|
||||
$stream = [System.IO.File]::Open($BackupPath, 'Open', 'Read', 'Read')
|
||||
$stream.Close()
|
||||
Write-Log "Backup integrity test PASSED (Database file readable)" -Level "SUCCESS"
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Backup integrity test FAILED: $_" -Level "ERROR"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN BACKUP FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Database Backup
|
||||
SQLite database backup and retention management
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
Write-Log "Backup started by: $env:USERNAME" -Level "INFO"
|
||||
Write-Log "Backup script: $($MyInvocation.MyCommand.Path)" -Level "INFO"
|
||||
|
||||
# Check if database exists
|
||||
if (-not (Test-DatabaseFile)) {
|
||||
Write-Log "Backup aborted: Database file not accessible" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get current database size
|
||||
$dbSize = Get-DatabaseSize
|
||||
Write-Log "Current database size: $dbSize KB" -Level "INFO"
|
||||
|
||||
try {
|
||||
# Perform backup
|
||||
$backupPath = Backup-Database
|
||||
|
||||
if ($backupPath) {
|
||||
# Test backup integrity
|
||||
$integrityOk = Test-BackupIntegrity -BackupPath $backupPath
|
||||
|
||||
if ($integrityOk) {
|
||||
# Cleanup old backups
|
||||
Remove-OldBackups
|
||||
|
||||
# Show statistics
|
||||
Get-BackupStatistics
|
||||
|
||||
Write-Log "Backup completed successfully" -Level "SUCCESS"
|
||||
exit 0
|
||||
} else {
|
||||
Write-Log "Backup created but failed integrity test" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Log "Backup failed" -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Backup process failed: $_" -Level "ERROR"
|
||||
Write-Log $_.ScriptStackTrace -Level "ERROR"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main backup
|
||||
Main
|
||||
585
deployment/windows/scripts/Build-Frontend.ps1
Normal file
585
deployment/windows/scripts/Build-Frontend.ps1
Normal file
@@ -0,0 +1,585 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build ROA2WEB Frontend for Production Deployment
|
||||
|
||||
.DESCRIPTION
|
||||
This script builds the Vue.js frontend for Windows Server deployment:
|
||||
- Checks for Node.js installation
|
||||
- Installs npm dependencies
|
||||
- Builds production-optimized static files
|
||||
- Creates deployment package with backend files
|
||||
- Optionally transfers to remote server
|
||||
|
||||
.PARAMETER BackendSource
|
||||
Path to backend source (default: ../../reports-app/backend)
|
||||
|
||||
.PARAMETER FrontendSource
|
||||
Path to frontend source (default: ../../reports-app/frontend)
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Output path for deployment package (default: ./deploy-package)
|
||||
|
||||
.PARAMETER ServerPath
|
||||
Remote server path for automatic deployment (optional)
|
||||
|
||||
.PARAMETER ServerHost
|
||||
Remote server hostname/IP for automatic deployment (optional)
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Frontend.ps1
|
||||
Build with defaults, output to ./deploy-package
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Frontend.ps1 -OutputPath "D:\deployments\roa2web-$(Get-Date -Format 'yyyyMMdd')"
|
||||
Build to custom output path
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-Frontend.ps1 -ServerHost "10.0.20.170" -ServerPath "C:\Temp\roa2web-deploy"
|
||||
Build and transfer to remote server
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: Node.js 16+, npm
|
||||
Can run on: WSL, Windows, Linux
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BackendSource = "../../reports-app/backend",
|
||||
[string]$FrontendSource = "../../reports-app/frontend",
|
||||
[string]$OutputPath = "./deploy-package",
|
||||
[string]$ServerHost = "",
|
||||
[string]$ServerPath = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-NodeJS {
|
||||
Write-Step "Checking Node.js installation..."
|
||||
|
||||
try {
|
||||
$nodeVersion = node --version 2>&1
|
||||
$npmVersion = npm --version 2>&1
|
||||
|
||||
Write-Success "Node.js: $nodeVersion"
|
||||
Write-Success "npm: $npmVersion"
|
||||
|
||||
# Check minimum version (16.x)
|
||||
if ($nodeVersion -match "v(\d+)\.") {
|
||||
$major = [int]$matches[1]
|
||||
if ($major -lt 16) {
|
||||
throw "Node.js version 16+ required (found: $nodeVersion)"
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Node.js not found or version too old"
|
||||
Write-Host "`n Install Node.js from: https://nodejs.org/" -ForegroundColor Yellow
|
||||
Write-Host " Minimum version: 16.x" -ForegroundColor Yellow
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-FullPath {
|
||||
param([string]$Path)
|
||||
|
||||
$scriptDir = Split-Path -Parent $PSScriptRoot
|
||||
$fullPath = Join-Path $scriptDir $Path
|
||||
|
||||
# Convert to absolute path and resolve .. and . properly
|
||||
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
|
||||
|
||||
return $fullPath
|
||||
}
|
||||
|
||||
function Build-Frontend {
|
||||
param([string]$SourcePath)
|
||||
|
||||
Write-Step "Building Vue.js frontend..."
|
||||
|
||||
if (-not (Test-Path $SourcePath)) {
|
||||
throw "Frontend source path not found: $SourcePath"
|
||||
}
|
||||
|
||||
Push-Location $SourcePath
|
||||
try {
|
||||
# Clean node_modules if it exists (to avoid EPERM errors)
|
||||
$nodeModulesPath = Join-Path $SourcePath "node_modules"
|
||||
if (Test-Path $nodeModulesPath) {
|
||||
Write-Step "Cleaning existing node_modules..."
|
||||
try {
|
||||
Remove-Item -Path $nodeModulesPath -Recurse -Force -ErrorAction Stop
|
||||
Write-Success "Cleaned node_modules"
|
||||
} catch {
|
||||
Write-Warning "Could not remove node_modules: $_"
|
||||
Write-Warning "Please close VS Code/IDE and try again, or run as Administrator"
|
||||
throw "Cannot proceed with locked node_modules. Close all IDEs and retry."
|
||||
}
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
Write-Step "Installing npm dependencies (this may take a minute)..."
|
||||
|
||||
# Show npm output for transparency, redirect to host to prevent capture
|
||||
npm install | Out-Default
|
||||
|
||||
# Verify node_modules was created
|
||||
if (-not (Test-Path $nodeModulesPath)) {
|
||||
throw "npm install failed: node_modules not created. Check errors above."
|
||||
}
|
||||
Write-Success "Dependencies installed"
|
||||
|
||||
# Build for production
|
||||
Write-Step "Building for production (this may take a minute)..."
|
||||
$env:NODE_ENV = "production"
|
||||
|
||||
# Show build output for transparency, redirect to host to prevent capture
|
||||
npm run build | Out-Default
|
||||
|
||||
Write-Success "Build completed"
|
||||
|
||||
# Verify dist folder
|
||||
$distPath = Join-Path $SourcePath "dist"
|
||||
if (-not (Test-Path $distPath)) {
|
||||
throw "Build failed: dist folder not found"
|
||||
}
|
||||
|
||||
$distFiles = Get-ChildItem -Path $distPath -Recurse -File
|
||||
$totalSize = ($distFiles | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
Write-Success "Generated $(($distFiles).Count) files (Total: $([math]::Round($totalSize, 2)) MB)"
|
||||
|
||||
return $distPath
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-BackendFiles {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying backend files..."
|
||||
|
||||
if (-not (Test-Path $SourcePath)) {
|
||||
throw "Backend source path not found: $SourcePath"
|
||||
}
|
||||
|
||||
# Ensure destination exists
|
||||
if (-not (Test-Path $DestPath)) {
|
||||
New-Item -ItemType Directory -Path $DestPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Exclude directory names (will skip entire directory trees)
|
||||
$excludeDirs = @(
|
||||
"venv",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"logs",
|
||||
"temp",
|
||||
"node_modules"
|
||||
)
|
||||
|
||||
# Exclude file patterns
|
||||
$excludeFiles = @(
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.log",
|
||||
".env",
|
||||
".env.local"
|
||||
)
|
||||
|
||||
# Normalize source path (ensure trailing backslash for proper substring calculation)
|
||||
$normalizedSourcePath = $SourcePath.TrimEnd('\', '/') + '\'
|
||||
|
||||
# Helper function to check if path should be excluded (using script: scope to access parent variables)
|
||||
$testExclude = {
|
||||
param([string]$RelativePath, [bool]$IsDirectory, [array]$ExcludeDirs, [array]$ExcludeFiles)
|
||||
|
||||
# Check directory exclusions (match directory name exactly)
|
||||
if ($IsDirectory) {
|
||||
$dirName = Split-Path $RelativePath -Leaf
|
||||
if ($ExcludeDirs -contains $dirName) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
# Check if any parent directory should be excluded
|
||||
$pathParts = $RelativePath -split '[\\/]'
|
||||
foreach ($part in $pathParts) {
|
||||
if ($ExcludeDirs -contains $part) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
# Check file patterns
|
||||
if (-not $IsDirectory) {
|
||||
foreach ($pattern in $ExcludeFiles) {
|
||||
if ($RelativePath -like $pattern) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
# Copy files
|
||||
Get-ChildItem -Path $SourcePath -Recurse | ForEach-Object {
|
||||
# Calculate relative path safely
|
||||
if ($_.FullName.Length -le $normalizedSourcePath.Length) {
|
||||
return # Skip if path is too short (shouldn't happen, but safety check)
|
||||
}
|
||||
$relativePath = $_.FullName.Substring($normalizedSourcePath.Length)
|
||||
|
||||
# Check if should be excluded
|
||||
$shouldExclude = & $testExclude -RelativePath $relativePath -IsDirectory $_.PSIsContainer -ExcludeDirs $excludeDirs -ExcludeFiles $excludeFiles
|
||||
if ($shouldExclude) {
|
||||
return
|
||||
}
|
||||
|
||||
$destFile = Join-Path $DestPath $relativePath
|
||||
|
||||
if ($_.PSIsContainer) {
|
||||
if (-not (Test-Path $destFile)) {
|
||||
New-Item -ItemType Directory -Path $destFile -Force | Out-Null
|
||||
}
|
||||
} else {
|
||||
$destDir = Split-Path $destFile -Parent
|
||||
if (-not (Test-Path $destDir)) {
|
||||
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
||||
}
|
||||
}
|
||||
|
||||
$backendFiles = Get-ChildItem -Path $DestPath -Recurse -File
|
||||
Write-Success "Copied $(($backendFiles).Count) backend files"
|
||||
}
|
||||
|
||||
function New-DeploymentPackage {
|
||||
param(
|
||||
[string]$FrontendDistPath,
|
||||
[string]$BackendSourcePath,
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
Write-Step "Creating deployment package..."
|
||||
|
||||
# Create output directory
|
||||
if (Test-Path $OutputPath) {
|
||||
Write-Warning "Output path exists, cleaning..."
|
||||
Remove-Item -Path $OutputPath -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
|
||||
|
||||
# Create structure
|
||||
$frontendDest = Join-Path $OutputPath "frontend"
|
||||
$backendDest = Join-Path $OutputPath "backend"
|
||||
|
||||
New-Item -ItemType Directory -Path $frontendDest -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $backendDest -Force | Out-Null
|
||||
|
||||
# Copy frontend dist
|
||||
Write-Step "Copying frontend files..."
|
||||
Copy-Item -Path "$FrontendDistPath\*" -Destination $frontendDest -Recurse -Force
|
||||
Write-Success "Frontend files copied"
|
||||
|
||||
# Copy backend files
|
||||
Copy-BackendFiles -SourcePath $BackendSourcePath -DestPath $backendDest
|
||||
|
||||
# Copy shared modules (database, auth, utils)
|
||||
Write-Step "Copying shared modules..."
|
||||
$sharedSource = Join-Path (Split-Path (Split-Path $BackendSourcePath -Parent) -Parent) "shared"
|
||||
$sharedDest = Join-Path $OutputPath "shared"
|
||||
|
||||
if (Test-Path $sharedSource) {
|
||||
Copy-Item -Path $sharedSource -Destination $sharedDest -Recurse -Force -Exclude @("__pycache__", "*.pyc", "tests")
|
||||
Write-Success "Shared modules copied"
|
||||
} else {
|
||||
Write-Warning "Shared modules not found at: $sharedSource"
|
||||
}
|
||||
|
||||
# Copy deployment config
|
||||
$configSource = Join-Path (Split-Path -Parent $PSScriptRoot) "config"
|
||||
$configDest = Join-Path $OutputPath "config"
|
||||
|
||||
if (Test-Path $configSource) {
|
||||
Copy-Item -Path $configSource -Destination $configDest -Recurse -Force
|
||||
Write-Success "Config files copied"
|
||||
}
|
||||
|
||||
# Copy deployment scripts
|
||||
Write-Step "Copying deployment scripts..."
|
||||
$scriptsSource = $PSScriptRoot
|
||||
$scriptsDest = Join-Path $OutputPath "scripts"
|
||||
New-Item -ItemType Directory -Path $scriptsDest -Force | Out-Null
|
||||
|
||||
# List of scripts to include in deployment package
|
||||
$deploymentScripts = @(
|
||||
"Install-ROA2WEB.ps1",
|
||||
"Deploy-ROA2WEB.ps1",
|
||||
"Start-ROA2WEB.ps1",
|
||||
"Stop-ROA2WEB.ps1",
|
||||
"Restart-ROA2WEB.ps1"
|
||||
)
|
||||
|
||||
$copiedScripts = 0
|
||||
foreach ($script in $deploymentScripts) {
|
||||
$scriptPath = Join-Path $scriptsSource $script
|
||||
if (Test-Path $scriptPath) {
|
||||
Copy-Item -Path $scriptPath -Destination $scriptsDest -Force
|
||||
$copiedScripts++
|
||||
}
|
||||
}
|
||||
Write-Success "Copied $copiedScripts deployment scripts"
|
||||
|
||||
# Create README
|
||||
$readmePath = Join-Path $OutputPath "README.txt"
|
||||
$readme = @"
|
||||
================================================================================
|
||||
ROA2WEB DEPLOYMENT PACKAGE
|
||||
Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
================================================================================
|
||||
|
||||
CONTENTS:
|
||||
---------
|
||||
backend/ FastAPI backend application files (Python)
|
||||
frontend/ Vue.js static files (built for production)
|
||||
config/ IIS configuration files (.env template, web.config)
|
||||
scripts/ PowerShell management scripts
|
||||
|
||||
DEPLOYMENT SCRIPTS:
|
||||
-------------------
|
||||
Install-ROA2WEB.ps1 First-time setup (Python venv, IIS site)
|
||||
Deploy-ROA2WEB.ps1 Update application files (auto-detects source)
|
||||
Start-ROA2WEB.ps1 Start backend service + IIS
|
||||
Stop-ROA2WEB.ps1 Stop backend service + IIS
|
||||
Restart-ROA2WEB.ps1 Quick restart
|
||||
|
||||
================================================================================
|
||||
DEPLOYMENT WORKFLOW
|
||||
================================================================================
|
||||
|
||||
>> FIRST TIME INSTALLATION:
|
||||
---------------------------
|
||||
1. Copy this entire folder to server (e.g., C:\Deploy\ROA2WEB-v1)
|
||||
|
||||
2. Open PowerShell as Administrator:
|
||||
cd C:\Deploy\ROA2WEB-v1\scripts
|
||||
.\Install-ROA2WEB.ps1
|
||||
|
||||
3. Configure environment:
|
||||
notepad C:\inetpub\wwwroot\roa2web\backend\.env
|
||||
|
||||
4. Start services:
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
5. Access: http://localhost:8080
|
||||
|
||||
|
||||
>> UPDATES (New code version):
|
||||
-------------------------------
|
||||
1. Copy new deployment package to server (e.g., C:\Deploy\ROA2WEB-v2)
|
||||
|
||||
2. Open PowerShell as Administrator:
|
||||
cd C:\Deploy\ROA2WEB-v2\scripts
|
||||
|
||||
3. Deploy (automatically stops, updates, and starts):
|
||||
.\Stop-ROA2WEB.ps1
|
||||
.\Deploy-ROA2WEB.ps1
|
||||
.\Start-ROA2WEB.ps1
|
||||
|
||||
Note: Deploy-ROA2WEB.ps1 auto-detects source path (no parameters needed!)
|
||||
|
||||
4. Done! Application updated with new code.
|
||||
|
||||
|
||||
>> QUICK OPERATIONS:
|
||||
--------------------
|
||||
Restart app: .\Restart-ROA2WEB.ps1
|
||||
Stop app: .\Stop-ROA2WEB.ps1
|
||||
Start app: .\Start-ROA2WEB.ps1
|
||||
|
||||
================================================================================
|
||||
REQUIREMENTS
|
||||
================================================================================
|
||||
- Windows Server 2016+ or Windows 10/11
|
||||
- IIS already installed (with ASP.NET Core Hosting Bundle)
|
||||
- Python 3.8+ installed
|
||||
- PowerShell 5.1+ (run as Administrator)
|
||||
|
||||
NOTES:
|
||||
------
|
||||
• Backend files do NOT include venv (virtual environment)
|
||||
• Install-ROA2WEB.ps1 creates venv and installs dependencies automatically
|
||||
• Deploy-ROA2WEB.ps1 creates backup before updating
|
||||
• .env files are preserved during updates
|
||||
• Application installs to: C:\inetpub\wwwroot\roa2web\
|
||||
|
||||
TROUBLESHOOTING:
|
||||
----------------
|
||||
Check logs: C:\inetpub\wwwroot\roa2web\logs\
|
||||
Backend logs: C:\inetpub\wwwroot\roa2web\backend\backend.log
|
||||
IIS logs: C:\inetpub\logs\LogFiles\
|
||||
|
||||
For detailed documentation, see: WINDOWS_DEPLOYMENT.md
|
||||
|
||||
================================================================================
|
||||
"@
|
||||
Set-Content -Path $readmePath -Value $readme -Force
|
||||
|
||||
# Calculate package size
|
||||
$packageFiles = Get-ChildItem -Path $OutputPath -Recurse -File
|
||||
$packageSize = ($packageFiles | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
|
||||
Write-Success "Deployment package created: $OutputPath"
|
||||
Write-Success "Total files: $(($packageFiles).Count)"
|
||||
Write-Success "Total size: $([math]::Round($packageSize, 2)) MB"
|
||||
|
||||
return $OutputPath
|
||||
}
|
||||
|
||||
function Copy-ToRemoteServer {
|
||||
param(
|
||||
[string]$LocalPath,
|
||||
[string]$ServerHost,
|
||||
[string]$ServerPath
|
||||
)
|
||||
|
||||
Write-Step "Transferring to remote server..."
|
||||
|
||||
try {
|
||||
# Check if remote server is accessible
|
||||
$pingResult = Test-Connection -ComputerName $ServerHost -Count 1 -Quiet
|
||||
|
||||
if (-not $pingResult) {
|
||||
Write-Warning "Server $ServerHost not reachable"
|
||||
return $false
|
||||
}
|
||||
|
||||
# Use robocopy for efficient transfer (Windows)
|
||||
if ($IsWindows -or $env:OS -match "Windows") {
|
||||
$remotePath = "\\$ServerHost\$($ServerPath -replace ':', '$')"
|
||||
|
||||
Write-Host " [*] Copying to: $remotePath" -ForegroundColor Yellow
|
||||
|
||||
robocopy $LocalPath $remotePath /E /Z /R:3 /W:5 /MT:8 /NFL /NDL /NP
|
||||
|
||||
if ($LASTEXITCODE -le 7) {
|
||||
Write-Success "Files transferred successfully"
|
||||
return $true
|
||||
} else {
|
||||
Write-Error "Transfer failed with code: $LASTEXITCODE"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
# Use scp for Unix/WSL
|
||||
Write-Warning "Remote copy via SCP not yet implemented"
|
||||
Write-Host " Manual transfer required to: $ServerHost`:$ServerPath" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to transfer to server: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN BUILD FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Frontend Build Script
|
||||
Build Vue.js frontend and create deployment package
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Resolve paths
|
||||
$backendSourcePath = Resolve-FullPath -Path $BackendSource
|
||||
$frontendSourcePath = Resolve-FullPath -Path $FrontendSource
|
||||
$outputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Resolve-FullPath -Path $OutputPath }
|
||||
|
||||
Write-Host "`nPaths:" -ForegroundColor Yellow
|
||||
Write-Host " Backend Source: $backendSourcePath"
|
||||
Write-Host " Frontend Source: $frontendSourcePath"
|
||||
Write-Host " Output Path: $outputPath"
|
||||
|
||||
# Check Node.js
|
||||
Test-NodeJS
|
||||
|
||||
# Build frontend
|
||||
$distPath = Build-Frontend -SourcePath $frontendSourcePath
|
||||
|
||||
# Create deployment package
|
||||
$packagePath = New-DeploymentPackage `
|
||||
-FrontendDistPath $distPath `
|
||||
-BackendSourcePath $backendSourcePath `
|
||||
-OutputPath $outputPath
|
||||
|
||||
# Transfer to server if specified
|
||||
if ($ServerHost -and $ServerPath) {
|
||||
$transferred = Copy-ToRemoteServer `
|
||||
-LocalPath $packagePath `
|
||||
-ServerHost $ServerHost `
|
||||
-ServerPath $ServerPath
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
Write-Host " BUILD COMPLETED SUCCESSFULLY" -ForegroundColor Green
|
||||
Write-Host ("=" * 70) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDeployment Package: $packagePath" -ForegroundColor Yellow
|
||||
|
||||
if ($ServerHost) {
|
||||
Write-Host "`nNext Steps (on server $ServerHost):" -ForegroundColor Yellow
|
||||
Write-Host " cd $ServerPath"
|
||||
Write-Host " .\Deploy-ROA2WEB.ps1 -SourcePath ."
|
||||
} else {
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Transfer '$packagePath' to your Windows Server"
|
||||
Write-Host " 2. On the server, run: Deploy-ROA2WEB.ps1 -SourcePath '<transferred-path>'"
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Build failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main build
|
||||
Main
|
||||
788
deployment/windows/scripts/Build-TelegramBot.ps1
Normal file
788
deployment/windows/scripts/Build-TelegramBot.ps1
Normal file
@@ -0,0 +1,788 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build ROA2WEB Telegram Bot for Windows Server Deployment
|
||||
|
||||
.DESCRIPTION
|
||||
This script creates a deployment package for the Telegram bot:
|
||||
- Copies application source files (app/)
|
||||
- Copies requirements.txt
|
||||
- Copies PowerShell deployment scripts
|
||||
- Creates .env.example template
|
||||
- Creates deployment README
|
||||
- Excludes development files (venv, data, logs, etc.)
|
||||
- Optionally transfers to remote server
|
||||
|
||||
.PARAMETER SourcePath
|
||||
Path to telegram-bot source (default: ../../reports-app/telegram-bot)
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Output path for deployment package (default: ../deploy-package/telegram-bot)
|
||||
|
||||
.PARAMETER ServerHost
|
||||
Remote server hostname/IP for automatic deployment (optional)
|
||||
|
||||
.PARAMETER ServerPath
|
||||
Remote server path for automatic deployment (optional)
|
||||
|
||||
.PARAMETER Clean
|
||||
Clean output directory before building (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-TelegramBot.ps1
|
||||
Build with defaults
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-TelegramBot.ps1 -OutputPath "D:\deployments\telegram-bot-$(Get-Date -Format 'yyyyMMdd')"
|
||||
Build to custom output path
|
||||
|
||||
.EXAMPLE
|
||||
.\Build-TelegramBot.ps1 -ServerHost "10.0.20.36" -ServerPath "C:\Temp\telegram-bot-deploy"
|
||||
Build and transfer to remote server
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+
|
||||
Can run on: WSL, Windows, Linux
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$SourcePath = "../../../reports-app/telegram-bot",
|
||||
[string]$OutputPath = "../deploy-package/telegram-bot",
|
||||
[string]$ServerHost = "",
|
||||
[string]$ServerPath = "",
|
||||
[bool]$Clean = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Resolve-FullPath {
|
||||
param([string]$Path)
|
||||
|
||||
$scriptDir = $PSScriptRoot
|
||||
$fullPath = Join-Path $scriptDir $Path
|
||||
|
||||
# Convert to absolute path and resolve .. and . properly
|
||||
$fullPath = [System.IO.Path]::GetFullPath($fullPath)
|
||||
|
||||
return $fullPath
|
||||
}
|
||||
|
||||
function Test-SourceDirectory {
|
||||
param([string]$Path)
|
||||
|
||||
Write-Step "Validating source directory..."
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
throw "Source path not found: $Path"
|
||||
}
|
||||
|
||||
# Check for required files/directories
|
||||
$requiredPaths = @(
|
||||
(Join-Path $Path "app"),
|
||||
(Join-Path $Path "requirements.txt")
|
||||
)
|
||||
|
||||
foreach ($reqPath in $requiredPaths) {
|
||||
if (-not (Test-Path $reqPath)) {
|
||||
throw "Required path not found: $reqPath"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Source directory validated: $Path"
|
||||
}
|
||||
|
||||
function New-CleanOutputDirectory {
|
||||
param([string]$Path)
|
||||
|
||||
if ($Clean -and (Test-Path $Path)) {
|
||||
Write-Step "Cleaning output directory..."
|
||||
Remove-Item -Path $Path -Recurse -Force
|
||||
Write-Success "Output directory cleaned"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
Write-Success "Created output directory: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-ApplicationFiles {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying application files..."
|
||||
|
||||
# Exclude patterns
|
||||
$excludeDirs = @(
|
||||
"venv",
|
||||
"data",
|
||||
"logs",
|
||||
"temp",
|
||||
"backups",
|
||||
"__pycache__",
|
||||
".pytest_cache",
|
||||
"tests",
|
||||
".git"
|
||||
)
|
||||
|
||||
$excludeFiles = @(
|
||||
".env",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.log",
|
||||
"*.db",
|
||||
".DS_Store",
|
||||
"Thumbs.db"
|
||||
)
|
||||
|
||||
# Create app directory in destination
|
||||
$destApp = Join-Path $DestPath "app"
|
||||
New-Item -ItemType Directory -Path $destApp -Force | Out-Null
|
||||
|
||||
# Copy app/ directory
|
||||
$sourceApp = Join-Path $SourcePath "app"
|
||||
$fileCount = 0
|
||||
|
||||
Get-ChildItem -Path $sourceApp -Recurse | ForEach-Object {
|
||||
# Check if in excluded directory
|
||||
$inExcludedDir = $false
|
||||
foreach ($excludeDir in $excludeDirs) {
|
||||
if ($_.FullName -like "*\$excludeDir\*" -or $_.FullName -like "*/$excludeDir/*") {
|
||||
$inExcludedDir = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($inExcludedDir) {
|
||||
return
|
||||
}
|
||||
|
||||
# Check if excluded file
|
||||
$isExcludedFile = $false
|
||||
foreach ($pattern in $excludeFiles) {
|
||||
if ($_.Name -like $pattern) {
|
||||
$isExcludedFile = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($isExcludedFile) {
|
||||
return
|
||||
}
|
||||
|
||||
# Calculate relative path and destination
|
||||
$relativePath = $_.FullName.Substring($sourceApp.Length)
|
||||
$destFile = Join-Path $destApp $relativePath
|
||||
|
||||
if ($_.PSIsContainer) {
|
||||
# Create directory
|
||||
if (-not (Test-Path $destFile)) {
|
||||
New-Item -ItemType Directory -Path $destFile -Force | Out-Null
|
||||
}
|
||||
} else {
|
||||
# Copy file
|
||||
$destFileDir = Split-Path $destFile -Parent
|
||||
if (-not (Test-Path $destFileDir)) {
|
||||
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
||||
$fileCount++
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Copied $fileCount application files"
|
||||
}
|
||||
|
||||
function Copy-RequirementsFile {
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying requirements.txt..."
|
||||
|
||||
$sourceReq = Join-Path $SourcePath "requirements.txt"
|
||||
$destReq = Join-Path $DestPath "requirements.txt"
|
||||
|
||||
if (Test-Path $sourceReq) {
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
Write-Success "requirements.txt copied"
|
||||
} else {
|
||||
Write-Warning "requirements.txt not found in source"
|
||||
}
|
||||
}
|
||||
|
||||
function New-EnvironmentTemplate {
|
||||
param([string]$DestPath)
|
||||
|
||||
Write-Step "Creating .env.example template..."
|
||||
|
||||
$envTemplate = @"
|
||||
# ROA2WEB Telegram Bot - Production Configuration Template
|
||||
|
||||
# Telegram Bot Configuration
|
||||
TELEGRAM_BOT_TOKEN=your_production_bot_token_from_@BotFather
|
||||
|
||||
# Claude API Configuration
|
||||
CLAUDE_API_KEY=your_production_claude_api_key_from_anthropic_console
|
||||
|
||||
# Backend API Configuration
|
||||
BACKEND_URL=http://localhost:8000
|
||||
BACKEND_TIMEOUT=30
|
||||
|
||||
# SQLite Database Configuration
|
||||
SQLITE_DB_PATH=C:\inetpub\wwwroot\roa2web\telegram-bot\data\telegram_bot.db
|
||||
|
||||
# Internal API Configuration (for backend callbacks)
|
||||
INTERNAL_API_HOST=127.0.0.1
|
||||
INTERNAL_API_PORT=8002
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=C:\inetpub\wwwroot\roa2web\telegram-bot\logs\bot.log
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
|
||||
# Authentication Configuration
|
||||
AUTH_CODE_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_THRESHOLD_MINUTES=5
|
||||
|
||||
# Session Configuration
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
MAX_CONVERSATION_HISTORY=20
|
||||
"@
|
||||
|
||||
$envPath = Join-Path $DestPath ".env.example"
|
||||
Set-Content -Path $envPath -Value $envTemplate -Encoding UTF8
|
||||
Write-Success ".env.example template created"
|
||||
}
|
||||
|
||||
function Copy-DeploymentScripts {
|
||||
param(
|
||||
[string]$SourceScriptsPath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying deployment scripts..."
|
||||
|
||||
$scriptsDir = Join-Path $DestPath "scripts"
|
||||
New-Item -ItemType Directory -Path $scriptsDir -Force | Out-Null
|
||||
|
||||
# List of scripts to copy
|
||||
$scripts = @(
|
||||
"Install-TelegramBot.ps1",
|
||||
"Deploy-TelegramBot.ps1",
|
||||
"Start-TelegramBot.ps1",
|
||||
"Stop-TelegramBot.ps1",
|
||||
"Restart-TelegramBot.ps1",
|
||||
"Backup-TelegramDB.ps1",
|
||||
"Setup-DailyBackup.ps1",
|
||||
"Setup-ClaudeAuth.ps1"
|
||||
)
|
||||
|
||||
$copiedCount = 0
|
||||
foreach ($script in $scripts) {
|
||||
$sourcePath = Join-Path $SourceScriptsPath $script
|
||||
if (Test-Path $sourcePath) {
|
||||
$destScript = Join-Path $scriptsDir $script
|
||||
Copy-Item -Path $sourcePath -Destination $destScript -Force
|
||||
$copiedCount++
|
||||
} else {
|
||||
Write-Warning "Script not found: $script"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Copied $copiedCount deployment scripts"
|
||||
}
|
||||
|
||||
function Copy-ConfigTemplates {
|
||||
param(
|
||||
[string]$SourceConfigPath,
|
||||
[string]$DestPath
|
||||
)
|
||||
|
||||
Write-Step "Copying configuration templates..."
|
||||
|
||||
$configDir = Join-Path $DestPath "config"
|
||||
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
|
||||
|
||||
# Copy .env.production.windows.telegram if it exists
|
||||
$prodEnv = Join-Path $SourceConfigPath ".env.production.windows.telegram"
|
||||
if (Test-Path $prodEnv) {
|
||||
Copy-Item -Path $prodEnv -Destination (Join-Path $configDir ".env.production.windows.telegram") -Force
|
||||
Write-Success "Production config template copied"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DeploymentReadme {
|
||||
param([string]$DestPath)
|
||||
|
||||
Write-Step "Creating deployment README..."
|
||||
|
||||
$readme = @"
|
||||
# ROA2WEB Telegram Bot - Deployment Package
|
||||
|
||||
**Created**: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||
**Version**: Production Deployment Package
|
||||
|
||||
## Contents
|
||||
|
||||
- ``app/`` - Telegram bot application source code
|
||||
- ``scripts/`` - PowerShell deployment and management scripts
|
||||
- ``config/`` - Configuration templates
|
||||
- ``requirements.txt`` - Python dependencies
|
||||
- ``.env.example`` - Environment configuration template
|
||||
- ``README.txt`` - This file
|
||||
|
||||
## Deployment Instructions
|
||||
|
||||
### 1. Initial Installation
|
||||
|
||||
Run as Administrator on Windows Server:
|
||||
|
||||
```powershell
|
||||
cd scripts
|
||||
.\Install-TelegramBot.ps1
|
||||
```
|
||||
|
||||
This will:
|
||||
- Check Python 3.11+ installation
|
||||
- Install NSSM (service manager)
|
||||
- Create directory structure
|
||||
- Create virtual environment
|
||||
- Install Python dependencies
|
||||
- Create Windows Service (ROA2WEB-TelegramBot)
|
||||
- Create configuration template
|
||||
|
||||
### 2. Configuration
|
||||
|
||||
Edit the ``.env`` file:
|
||||
|
||||
```powershell
|
||||
notepad C:\inetpub\wwwroot\roa2web\telegram-bot\.env
|
||||
```
|
||||
|
||||
Required settings:
|
||||
- ``TELEGRAM_BOT_TOKEN`` - Get from @BotFather on Telegram
|
||||
- ``CLAUDE_API_KEY`` - Get from Anthropic console
|
||||
- ``BACKEND_URL`` - Usually http://localhost:8000
|
||||
|
||||
### 3. Start Service
|
||||
|
||||
```powershell
|
||||
cd C:\inetpub\wwwroot\roa2web\telegram-bot\scripts
|
||||
.\Start-TelegramBot.ps1
|
||||
```
|
||||
|
||||
### 4. Verify Deployment
|
||||
|
||||
Check health endpoint:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest http://localhost:8002/internal/health
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stdout.log -Tail 50 -Wait
|
||||
```
|
||||
|
||||
## Management Commands
|
||||
|
||||
- **Start**: ``.\Start-TelegramBot.ps1``
|
||||
- **Stop**: ``.\Stop-TelegramBot.ps1``
|
||||
- **Restart**: ``.\Restart-TelegramBot.ps1``
|
||||
- **Deploy Update**: ``.\Deploy-TelegramBot.ps1 -SourcePath "path\to\new\package"``
|
||||
- **Backup Database**: ``.\Backup-TelegramDB.ps1``
|
||||
- **Setup Daily Backup**: ``.\Setup-DailyBackup.ps1``
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
C:\inetpub\wwwroot\roa2web\telegram-bot\
|
||||
├── app\ # Application source code
|
||||
├── venv\ # Python virtual environment
|
||||
├── data\ # SQLite database (telegram_bot.db)
|
||||
├── logs\ # Application logs
|
||||
├── backups\ # Database backups
|
||||
├── temp\ # Temporary files
|
||||
├── scripts\ # Management scripts
|
||||
├── config\ # Configuration templates
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Environment configuration
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Service won't start
|
||||
|
||||
Check logs:
|
||||
|
||||
```powershell
|
||||
Get-Content C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log -Tail 100
|
||||
```
|
||||
|
||||
### Bot not responding on Telegram
|
||||
|
||||
1. Verify service is running: ``Get-Service ROA2WEB-TelegramBot``
|
||||
2. Check health endpoint: ``Invoke-WebRequest http://localhost:8002/internal/health``
|
||||
3. Verify ``.env`` configuration (TELEGRAM_BOT_TOKEN)
|
||||
4. Check logs for errors
|
||||
|
||||
### Database errors
|
||||
|
||||
Run database backup and check integrity:
|
||||
|
||||
```powershell
|
||||
.\Backup-TelegramDB.ps1
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: ``C:\inetpub\wwwroot\roa2web\deployment\windows\docs\TELEGRAM_BOT_DEPLOYMENT.md``
|
||||
- Project repository: ROA2WEB on GitHub
|
||||
- Contact: ROA2WEB Team
|
||||
|
||||
"@
|
||||
|
||||
$readmePath = Join-Path $DestPath "README.txt"
|
||||
Set-Content -Path $readmePath -Value $readme -Encoding UTF8
|
||||
Write-Success "Deployment README created"
|
||||
}
|
||||
|
||||
function Copy-ClaudeCredentials {
|
||||
param([string]$PackagePath)
|
||||
|
||||
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host " OPTIONAL: Claude Credentials" -ForegroundColor Yellow
|
||||
Write-Host ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "If you have Claude Pro/Max credentials from 'claude-code login',"
|
||||
Write-Host "you can include them in the deployment package for easy setup."
|
||||
Write-Host ""
|
||||
|
||||
$response = Read-Host "Copy Claude credentials to deployment package? (Y/N)"
|
||||
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
# Try to find credentials automatically in both possible locations
|
||||
$possiblePaths = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\.credentials.json"), # Correct location
|
||||
(Join-Path $env:APPDATA "claude\credentials.json") # Alternative location
|
||||
)
|
||||
|
||||
$defaultCredPath = $null
|
||||
foreach ($path in $possiblePaths) {
|
||||
if (Test-Path $path) {
|
||||
$defaultCredPath = $path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($defaultCredPath) {
|
||||
Write-Host "`nFound credentials at: $defaultCredPath" -ForegroundColor Green
|
||||
$usePath = Read-Host "Use this path? (Y/N)"
|
||||
|
||||
if ($usePath -eq "Y" -or $usePath -eq "y") {
|
||||
$credPath = $defaultCredPath
|
||||
} else {
|
||||
$credPath = Read-Host "Enter path to credentials.json"
|
||||
}
|
||||
} else {
|
||||
Write-Host "`nCredentials not found at default locations" -ForegroundColor Yellow
|
||||
Write-Host " Checked: $($possiblePaths -join ', ')" -ForegroundColor Gray
|
||||
$credPath = Read-Host "Enter full path to credentials.json"
|
||||
}
|
||||
|
||||
if (Test-Path $credPath) {
|
||||
try {
|
||||
$destCredPath = Join-Path $PackagePath "claude-credentials.json"
|
||||
Copy-Item -Path $credPath -Destination $destCredPath -Force
|
||||
Write-Success "Claude credentials copied to deployment package"
|
||||
Write-Host " Location: $destCredPath" -ForegroundColor Gray
|
||||
Write-Host " The Setup-ClaudeAuth.ps1 script will detect and use this file automatically" -ForegroundColor Gray
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to copy credentials: $_"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Credentials file not found at: $credPath"
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "Skipping credentials copy. You can set up Claude auth manually on the server." -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Transfer-ToServerSSH {
|
||||
param([string]$PackagePath)
|
||||
|
||||
Write-Host "`n" + ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host " OPTIONAL: SSH Transfer to Server" -ForegroundColor Yellow
|
||||
Write-Host ("=" * 60) -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "You can automatically transfer the deployment package to"
|
||||
Write-Host "the Windows Server via SSH/SCP (requires SSH server on Windows)."
|
||||
Write-Host ""
|
||||
|
||||
$response = Read-Host "Transfer package to server via SSH? (Y/N)"
|
||||
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
Write-Host ""
|
||||
$sshUser = Read-Host "Enter SSH username (e.g., Administrator)"
|
||||
$sshHost = Read-Host "Enter server hostname/IP (e.g., 10.0.20.36)"
|
||||
$sshPort = Read-Host "Enter SSH port (default: 22, press Enter for default)"
|
||||
if ([string]::IsNullOrWhiteSpace($sshPort)) {
|
||||
$sshPort = "22"
|
||||
}
|
||||
|
||||
$remotePath = Read-Host "Enter remote path (e.g., C:/Temp/telegram-bot-deploy)"
|
||||
|
||||
# Convert Windows path to SCP format if needed
|
||||
$remotePath = $remotePath -replace '\\', '/'
|
||||
if ($remotePath -match '^[A-Za-z]:') {
|
||||
# Convert C:/path to /c/path format for SCP
|
||||
$remotePath = $remotePath -replace '^([A-Za-z]):', '/$1'
|
||||
}
|
||||
|
||||
Write-Host "`nTransfer Configuration:" -ForegroundColor Cyan
|
||||
Write-Host " Source: $PackagePath"
|
||||
Write-Host " Target: ${sshUser}@${sshHost}:${remotePath}"
|
||||
Write-Host " Port: $sshPort"
|
||||
Write-Host ""
|
||||
|
||||
$confirm = Read-Host "Proceed with transfer? (Y/N)"
|
||||
|
||||
if ($confirm -eq "Y" -or $confirm -eq "y") {
|
||||
Write-Step "Transferring package via SCP..."
|
||||
|
||||
try {
|
||||
# Use SCP to transfer
|
||||
$scpTarget = "${sshUser}@${sshHost}:${remotePath}"
|
||||
|
||||
# Build SCP command
|
||||
$scpArgs = @(
|
||||
"-P", $sshPort,
|
||||
"-r",
|
||||
$PackagePath,
|
||||
$scpTarget
|
||||
)
|
||||
|
||||
Write-Host " Running: scp $scpArgs" -ForegroundColor Gray
|
||||
|
||||
& scp @scpArgs
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Package transferred successfully!"
|
||||
Write-Host " Remote location: $scpTarget" -ForegroundColor Gray
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "SCP transfer failed with exit code: $LASTEXITCODE"
|
||||
Write-Host " You can transfer manually via RDP or network share" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Transfer failed: $_"
|
||||
Write-Host " Make sure 'scp' is available in PATH" -ForegroundColor Yellow
|
||||
Write-Host " Alternative: Use WinSCP, FileZilla, or manual RDP copy" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "Transfer cancelled" -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
} else {
|
||||
Write-Host "Skipping SSH transfer. Transfer package manually via RDP or network share." -ForegroundColor Gray
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-PackageSummary {
|
||||
param(
|
||||
[string]$PackagePath,
|
||||
[bool]$CredentialsCopied,
|
||||
[bool]$TransferredToServer
|
||||
)
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " DEPLOYMENT PACKAGE CREATED SUCCESSFULLY" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nPackage Location:" -ForegroundColor Yellow
|
||||
Write-Host " $PackagePath"
|
||||
|
||||
# Calculate package size
|
||||
$files = Get-ChildItem -Path $PackagePath -Recurse -File
|
||||
$totalSize = ($files | Measure-Object -Property Length -Sum).Sum / 1MB
|
||||
|
||||
Write-Host "`nPackage Contents:" -ForegroundColor Yellow
|
||||
Write-Host " Files: $($files.Count)"
|
||||
Write-Host " Total Size: $([math]::Round($totalSize, 2)) MB"
|
||||
if ($CredentialsCopied) {
|
||||
Write-Host " ✓ Claude credentials included" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($TransferredToServer) {
|
||||
Write-Host "`nDeployment Status:" -ForegroundColor Yellow
|
||||
Write-Host " ✓ Package transferred to server via SSH" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next Steps on Server:" -ForegroundColor Yellow
|
||||
Write-Host " 1. SSH to server or RDP"
|
||||
Write-Host " 2. Navigate to deployment location"
|
||||
Write-Host " 3. Run: scripts\Install-TelegramBot.ps1 (as Administrator)"
|
||||
Write-Host " 4. Run: scripts\Setup-ClaudeAuth.ps1 (will auto-detect credentials)"
|
||||
Write-Host " 5. Configure: .env file with Telegram bot token"
|
||||
Write-Host " 6. Run: scripts\Start-TelegramBot.ps1"
|
||||
} else {
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Transfer package to Windows Server (10.0.20.36)"
|
||||
Write-Host " - Via network share: Copy-Item -Path $PackagePath -Destination \\10.0.20.36\C$\Temp\telegram-bot-deploy -Recurse"
|
||||
Write-Host " - Via RDP: Manual copy"
|
||||
Write-Host " 2. On server, run: scripts\Install-TelegramBot.ps1 (as Administrator)"
|
||||
Write-Host " 3. Configure: .env file with production credentials"
|
||||
Write-Host " 4. Run: scripts\Start-TelegramBot.ps1"
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Transfer-ToServer {
|
||||
param(
|
||||
[string]$PackagePath,
|
||||
[string]$ServerHost,
|
||||
[string]$ServerPath
|
||||
)
|
||||
|
||||
if (-not $ServerHost -or -not $ServerPath) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Transferring to server $ServerHost..."
|
||||
|
||||
try {
|
||||
# Check if server is reachable
|
||||
if (Test-Connection -ComputerName $ServerHost -Count 1 -Quiet) {
|
||||
Write-Success "Server is reachable"
|
||||
} else {
|
||||
Write-Warning "Server is not reachable, skipping transfer"
|
||||
return
|
||||
}
|
||||
|
||||
# Transfer files (using Copy-Item for network path or RoboCopy)
|
||||
$networkPath = "\\$ServerHost\$(($ServerPath -replace ':', '$'))"
|
||||
|
||||
Write-Step "Copying to: $networkPath"
|
||||
|
||||
# Ensure destination exists
|
||||
if (-not (Test-Path $networkPath)) {
|
||||
New-Item -ItemType Directory -Path $networkPath -Force | Out-Null
|
||||
}
|
||||
|
||||
# Copy package
|
||||
Copy-Item -Path "$PackagePath\*" -Destination $networkPath -Recurse -Force
|
||||
|
||||
Write-Success "Package transferred to server"
|
||||
} catch {
|
||||
Write-Warning "Failed to transfer to server: $_"
|
||||
Write-Host " You can manually copy the package from: $PackagePath" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN BUILD FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Build Deployment Package
|
||||
Creating production-ready deployment package
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Resolve paths
|
||||
$sourcePath = Resolve-FullPath -Path $SourcePath
|
||||
$outputPath = Resolve-FullPath -Path $OutputPath
|
||||
$scriptsPath = $PSScriptRoot
|
||||
|
||||
Write-Step "Build Configuration"
|
||||
Write-Host " Source: $sourcePath" -ForegroundColor Gray
|
||||
Write-Host " Output: $outputPath" -ForegroundColor Gray
|
||||
|
||||
try {
|
||||
# Build steps
|
||||
Test-SourceDirectory -Path $sourcePath
|
||||
New-CleanOutputDirectory -Path $outputPath
|
||||
Copy-ApplicationFiles -SourcePath $sourcePath -DestPath $outputPath
|
||||
Copy-RequirementsFile -SourcePath $sourcePath -DestPath $outputPath
|
||||
New-EnvironmentTemplate -DestPath $outputPath
|
||||
Copy-DeploymentScripts -SourceScriptsPath $scriptsPath -DestPath $outputPath
|
||||
|
||||
# Copy config templates if they exist
|
||||
$configPath = Join-Path (Split-Path $scriptsPath -Parent) "config"
|
||||
if (Test-Path $configPath) {
|
||||
Copy-ConfigTemplates -SourceConfigPath $configPath -DestPath $outputPath
|
||||
}
|
||||
|
||||
New-DeploymentReadme -DestPath $outputPath
|
||||
|
||||
# Interactive: Copy Claude credentials to package
|
||||
$credentialsCopied = Copy-ClaudeCredentials -PackagePath $outputPath
|
||||
|
||||
# Interactive: Transfer to server via SSH
|
||||
$transferredToServer = $false
|
||||
if (-not ($ServerHost -and $ServerPath)) {
|
||||
# Interactive SSH transfer
|
||||
$transferredToServer = Transfer-ToServerSSH -PackagePath $outputPath
|
||||
} else {
|
||||
# Legacy non-interactive transfer (if parameters provided)
|
||||
Transfer-ToServer -PackagePath $outputPath -ServerHost $ServerHost -ServerPath $ServerPath
|
||||
$transferredToServer = $true
|
||||
}
|
||||
|
||||
# Show summary with deployment status
|
||||
Show-PackageSummary -PackagePath $outputPath -CredentialsCopied $credentialsCopied -TransferredToServer $transferredToServer
|
||||
|
||||
Write-Host "`nBuild completed successfully!" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[BUILD FAILED] $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main build
|
||||
Main
|
||||
496
deployment/windows/scripts/Deploy-ROA2WEB.ps1
Normal file
496
deployment/windows/scripts/Deploy-ROA2WEB.ps1
Normal file
@@ -0,0 +1,496 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB - Quick Deployment/Update Script for Windows Server
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs rapid deployment or updates of ROA2WEB application:
|
||||
- Auto-detects source path (use from scripts/ directory)
|
||||
- Creates backup of current deployment
|
||||
- Stops backend service
|
||||
- Updates backend and/or frontend files
|
||||
- Installs new Python dependencies if changed
|
||||
- Restarts backend service
|
||||
- Validates deployment health
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Target installation path (default: C:\inetpub\wwwroot\roa2web)
|
||||
|
||||
.PARAMETER BackupEnabled
|
||||
Create backup before deployment (default: true)
|
||||
|
||||
.PARAMETER RestartService
|
||||
Restart backend service after deployment (default: true)
|
||||
|
||||
.PARAMETER UpdateBackend
|
||||
Update backend files (default: true)
|
||||
|
||||
.PARAMETER UpdateFrontend
|
||||
Update frontend files (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
cd C:\Deploy\ROA2WEB\scripts
|
||||
.\Deploy-ROA2WEB.ps1
|
||||
Deploy from current deployment package (auto-detected)
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-ROA2WEB.ps1 -UpdateBackend -UpdateFrontend:$false
|
||||
Update only backend files
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
Must be run from deployment package's scripts/ directory
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
|
||||
[bool]$BackupEnabled = $true,
|
||||
[bool]$RestartService = $true,
|
||||
[bool]$UpdateBackend = $true,
|
||||
[bool]$UpdateFrontend = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect source path: if running from scripts/ subdirectory, use parent
|
||||
$detectedSourcePath = $PSScriptRoot
|
||||
if ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
|
||||
$detectedSourcePath = Split-Path $PSScriptRoot -Parent
|
||||
}
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB"
|
||||
ServiceName = "ROA2WEB-Backend"
|
||||
InstallPath = $InstallPath
|
||||
BackendPath = Join-Path $InstallPath "backend"
|
||||
FrontendPath = Join-Path $InstallPath "frontend"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
SourcePath = $detectedSourcePath
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function New-BackupDirectory {
|
||||
if (-not (Test-Path $Config.BackupPath)) {
|
||||
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Backup-CurrentDeployment {
|
||||
if (-not $BackupEnabled) {
|
||||
Write-Warning "Backup disabled, skipping..."
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Step "Creating backup of current deployment..."
|
||||
|
||||
New-BackupDirectory
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupName = "backup-$timestamp"
|
||||
$backupFullPath = Join-Path $Config.BackupPath $backupName
|
||||
|
||||
try {
|
||||
# Create backup directory
|
||||
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
|
||||
|
||||
# Backup backend
|
||||
if ((Test-Path $Config.BackendPath) -and $UpdateBackend) {
|
||||
$backupBackendPath = Join-Path $backupFullPath "backend"
|
||||
Copy-Item -Path $Config.BackendPath -Destination $backupBackendPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Success "Backend backed up"
|
||||
}
|
||||
|
||||
# Backup frontend
|
||||
if ((Test-Path $Config.FrontendPath) -and $UpdateFrontend) {
|
||||
$backupFrontendPath = Join-Path $backupFullPath "frontend"
|
||||
Copy-Item -Path $Config.FrontendPath -Destination $backupFrontendPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Success "Frontend backed up"
|
||||
}
|
||||
|
||||
Write-Success "Backup created at: $backupFullPath"
|
||||
|
||||
# Clean old backups (keep last 10)
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -Directory | Sort-Object Name -Descending
|
||||
if ($allBackups.Count -gt 10) {
|
||||
$oldBackups = $allBackups | Select-Object -Skip 10
|
||||
foreach ($oldBackup in $oldBackups) {
|
||||
Remove-Item -Path $oldBackup.FullName -Recurse -Force
|
||||
Write-Success "Cleaned old backup: $($oldBackup.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
return $backupFullPath
|
||||
} catch {
|
||||
Write-Error "Backup failed: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-BackendService {
|
||||
Write-Step "Stopping backend service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Warning "Service $($Config.ServiceName) not found"
|
||||
return
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Stop-Service -Name $Config.ServiceName -Force
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Wait for service to stop
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Success "Service stopped successfully"
|
||||
} else {
|
||||
Write-Warning "Service did not stop within timeout"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Service already stopped"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to stop service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Update-BackendFiles {
|
||||
if (-not $UpdateBackend) {
|
||||
Write-Warning "Backend update disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Updating backend files..."
|
||||
|
||||
$sourceBackend = Join-Path $Config.SourcePath "backend"
|
||||
|
||||
if (-not (Test-Path $sourceBackend)) {
|
||||
Write-Warning "Source backend path not found: $sourceBackend"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Copy all files except .env (preserve existing config)
|
||||
$excludeFiles = @("*.env", "*.log", "*.pyc", "__pycache__")
|
||||
|
||||
Get-ChildItem -Path $sourceBackend -Recurse -File | ForEach-Object {
|
||||
$relativePath = $_.FullName.Substring($sourceBackend.Length)
|
||||
$destPath = Join-Path $Config.BackendPath $relativePath
|
||||
|
||||
# Skip excluded files
|
||||
$skip = $false
|
||||
foreach ($pattern in $excludeFiles) {
|
||||
if ($_.Name -like $pattern -or $_.Directory.Name -eq "__pycache__") {
|
||||
$skip = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $skip) {
|
||||
$destDir = Split-Path $destPath -Parent
|
||||
if (-not (Test-Path $destDir)) {
|
||||
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $_.FullName -Destination $destPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "Backend files updated"
|
||||
|
||||
# Check if requirements.txt changed
|
||||
$sourceReq = Join-Path $sourceBackend "requirements.txt"
|
||||
$destReq = Join-Path $Config.BackendPath "requirements.txt"
|
||||
|
||||
if (Test-Path $sourceReq) {
|
||||
$sourceHash = (Get-FileHash $sourceReq).Hash
|
||||
$destHash = if (Test-Path $destReq) { (Get-FileHash $destReq).Hash } else { "" }
|
||||
|
||||
if ($sourceHash -ne $destHash) {
|
||||
Write-Step "Requirements changed, updating Python dependencies..."
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
|
||||
try {
|
||||
& python -m pip install -r $destReq --upgrade
|
||||
Write-Success "Python dependencies updated"
|
||||
} catch {
|
||||
Write-Error "Failed to update Python dependencies: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Python dependencies unchanged"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to update backend files: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Update-FrontendFiles {
|
||||
if (-not $UpdateFrontend) {
|
||||
Write-Warning "Frontend update disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Updating frontend files..."
|
||||
|
||||
$sourceFrontend = Join-Path $Config.SourcePath "frontend"
|
||||
|
||||
if (-not (Test-Path $sourceFrontend)) {
|
||||
Write-Warning "Source frontend path not found: $sourceFrontend"
|
||||
Write-Warning "Expected path: $sourceFrontend"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Remove old frontend files (except web.config)
|
||||
if (Test-Path $Config.FrontendPath) {
|
||||
Get-ChildItem -Path $Config.FrontendPath -Exclude "web.config" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Copy new frontend files
|
||||
Copy-Item -Path "$sourceFrontend\*" -Destination $Config.FrontendPath -Recurse -Force
|
||||
|
||||
# Ensure web.config exists
|
||||
$webConfigPath = Join-Path $Config.FrontendPath "web.config"
|
||||
$webConfigTemplate = Join-Path (Split-Path $PSScriptRoot -Parent) "config\web.config"
|
||||
|
||||
if (-not (Test-Path $webConfigPath) -and (Test-Path $webConfigTemplate)) {
|
||||
Copy-Item -Path $webConfigTemplate -Destination $webConfigPath -Force
|
||||
Write-Success "web.config restored from template"
|
||||
}
|
||||
|
||||
Write-Success "Frontend files updated"
|
||||
} catch {
|
||||
Write-Error "Failed to update frontend files: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Start-BackendService {
|
||||
if (-not $RestartService) {
|
||||
Write-Warning "Service restart disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Starting backend service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Error "Service $($Config.ServiceName) not found"
|
||||
return
|
||||
}
|
||||
|
||||
Start-Service -Name $Config.ServiceName
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Wait for service to start
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Success "Service started successfully"
|
||||
} else {
|
||||
Write-Error "Service failed to start (Status: $($service.Status))"
|
||||
Write-Warning "Check logs at: $($Config.LogsPath)\backend-stderr.log"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to start service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DeploymentHealth {
|
||||
Write-Step "Testing deployment health..."
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
# Get service port from .env or use default
|
||||
$envFile = Join-Path $Config.BackendPath ".env"
|
||||
$port = 8000
|
||||
|
||||
if (Test-Path $envFile) {
|
||||
$portLine = Get-Content $envFile | Where-Object { $_ -match "^PORT=(\d+)" }
|
||||
if ($portLine) {
|
||||
$port = [int]$matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
# Test backend health endpoint
|
||||
$healthUrl = "http://localhost:$port/health"
|
||||
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
|
||||
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Success "Backend health check passed"
|
||||
Write-Success "Response: $($response.Content)"
|
||||
} else {
|
||||
Write-Warning "Health check returned status: $($response.StatusCode)"
|
||||
}
|
||||
|
||||
# Test frontend (IIS)
|
||||
try {
|
||||
$frontendResponse = Invoke-WebRequest -Uri "http://localhost/" -UseBasicParsing -TimeoutSec 5
|
||||
if ($frontendResponse.StatusCode -eq 200) {
|
||||
Write-Success "Frontend health check passed"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Frontend health check failed: $_"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Health check failed: $_"
|
||||
Write-Warning "The service may need more time to start"
|
||||
Write-Warning "Check logs: $($Config.LogsPath)\backend-stderr.log"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-DeploymentSummary {
|
||||
param([string]$BackupPath, [datetime]$StartTime)
|
||||
|
||||
$duration = (Get-Date) - $StartTime
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " DEPLOYMENT COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDeployment Summary:" -ForegroundColor Yellow
|
||||
Write-Host " Duration: $($duration.TotalSeconds) seconds"
|
||||
Write-Host " Backend Updated: $UpdateBackend"
|
||||
Write-Host " Frontend Updated: $UpdateFrontend"
|
||||
if ($BackupPath) {
|
||||
Write-Host " Backup Location: $BackupPath"
|
||||
}
|
||||
|
||||
Write-Host "`nApplication Status:" -ForegroundColor Yellow
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
if ($service) {
|
||||
Write-Host " Backend Service: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" })
|
||||
}
|
||||
} catch {
|
||||
Write-Host " Backend Service: Unknown" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`nAccess Points:" -ForegroundColor Yellow
|
||||
Write-Host " Web Application: http://localhost"
|
||||
Write-Host " API Documentation: http://localhost/api/docs"
|
||||
|
||||
Write-Host "`nManagement:" -ForegroundColor Yellow
|
||||
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50 -Wait"
|
||||
Write-Host " Restart Service: .\Restart-ROA2WEB.ps1"
|
||||
Write-Host " Rollback: Use backup at $BackupPath"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN DEPLOYMENT FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Deployment Script
|
||||
Fast deployment and updates for Windows Server + IIS
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
$startTime = Get-Date
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
if (-not (Test-Path $Config.InstallPath)) {
|
||||
Write-Error "Installation path not found: $($Config.InstallPath)"
|
||||
Write-Host " Run Install-ROA2WEB.ps1 first" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Installation path exists"
|
||||
|
||||
try {
|
||||
# Deployment steps
|
||||
$backupPath = Backup-CurrentDeployment
|
||||
Stop-BackendService
|
||||
Update-BackendFiles
|
||||
Update-FrontendFiles
|
||||
Start-BackendService
|
||||
Test-DeploymentHealth
|
||||
Show-DeploymentSummary -BackupPath $backupPath -StartTime $startTime
|
||||
|
||||
Write-Host "`nDeployment completed successfully!" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Deployment failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
|
||||
# Attempt rollback if backup exists
|
||||
if ($backupPath -and (Test-Path $backupPath)) {
|
||||
Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow
|
||||
# TODO: Implement rollback logic
|
||||
}
|
||||
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main deployment
|
||||
Main
|
||||
598
deployment/windows/scripts/Deploy-TelegramBot.ps1
Normal file
598
deployment/windows/scripts/Deploy-TelegramBot.ps1
Normal file
@@ -0,0 +1,598 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB Telegram Bot - Quick Deployment/Update Script for Windows Server
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs rapid deployment or updates of ROA2WEB Telegram Bot:
|
||||
- Auto-detects source path (use from scripts/ directory)
|
||||
- Creates backup of current deployment (app files + database)
|
||||
- Stops bot service
|
||||
- Updates application files
|
||||
- Installs new Python dependencies if changed
|
||||
- Preserves .env configuration
|
||||
- Restarts bot service
|
||||
- Validates deployment health
|
||||
- Rollback support on failure
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Target installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER SourcePath
|
||||
Source path for deployment package (auto-detected if run from scripts/)
|
||||
|
||||
.PARAMETER BackupEnabled
|
||||
Create backup before deployment (default: true)
|
||||
|
||||
.PARAMETER RestartService
|
||||
Restart bot service after deployment (default: true)
|
||||
|
||||
.PARAMETER RollbackOnFailure
|
||||
Automatically rollback if deployment fails (default: true)
|
||||
|
||||
.EXAMPLE
|
||||
cd C:\Deploy\TelegramBot\scripts
|
||||
.\Deploy-TelegramBot.ps1
|
||||
Deploy from current deployment package (auto-detected)
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-TelegramBot.ps1 -SourcePath "C:\Deploy\new-version"
|
||||
Deploy from specific source path
|
||||
|
||||
.EXAMPLE
|
||||
.\Deploy-TelegramBot.ps1 -BackupEnabled $false -RestartService $false
|
||||
Update files without backup or restart (manual testing)
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
Recommended to run from deployment package's scripts/ directory
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[string]$SourcePath = "",
|
||||
[bool]$BackupEnabled = $true,
|
||||
[bool]$RestartService = $true,
|
||||
[bool]$RollbackOnFailure = $true
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect source path: if running from scripts/ subdirectory, use parent
|
||||
$detectedSourcePath = if ($SourcePath) {
|
||||
$SourcePath
|
||||
} elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
|
||||
Split-Path $PSScriptRoot -Parent
|
||||
} else {
|
||||
$PSScriptRoot
|
||||
}
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB-TelegramBot"
|
||||
ServiceName = "ROA2WEB-TelegramBot"
|
||||
InstallPath = $InstallPath
|
||||
DataPath = Join-Path $InstallPath "data"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
SourcePath = $detectedSourcePath
|
||||
}
|
||||
|
||||
$script:DeploymentState = @{
|
||||
BackupPath = $null
|
||||
ServiceWasRunning = $false
|
||||
DeploymentSuccess = $false
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function New-BackupDirectory {
|
||||
if (-not (Test-Path $Config.BackupPath)) {
|
||||
New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Backup-CurrentDeployment {
|
||||
if (-not $BackupEnabled) {
|
||||
Write-Warning "Backup disabled, skipping..."
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Step "Creating backup of current deployment..."
|
||||
|
||||
New-BackupDirectory
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$backupName = "backup-$timestamp"
|
||||
$backupFullPath = Join-Path $Config.BackupPath $backupName
|
||||
|
||||
try {
|
||||
# Create backup directory
|
||||
New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null
|
||||
|
||||
# Backup app directory
|
||||
if (Test-Path (Join-Path $Config.InstallPath "app")) {
|
||||
$backupAppPath = Join-Path $backupFullPath "app"
|
||||
Copy-Item -Path (Join-Path $Config.InstallPath "app") -Destination $backupAppPath -Recurse -Force
|
||||
Write-Success "App files backed up"
|
||||
}
|
||||
|
||||
# Backup requirements.txt
|
||||
$reqFile = Join-Path $Config.InstallPath "requirements.txt"
|
||||
if (Test-Path $reqFile) {
|
||||
Copy-Item -Path $reqFile -Destination (Join-Path $backupFullPath "requirements.txt") -Force
|
||||
Write-Success "Requirements file backed up"
|
||||
}
|
||||
|
||||
# Backup .env file
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
if (Test-Path $envFile) {
|
||||
Copy-Item -Path $envFile -Destination (Join-Path $backupFullPath ".env") -Force
|
||||
Write-Success ".env file backed up"
|
||||
}
|
||||
|
||||
# Backup database
|
||||
$dbFile = Join-Path $Config.DataPath "telegram_bot.db"
|
||||
if (Test-Path $dbFile) {
|
||||
$backupDataPath = Join-Path $backupFullPath "data"
|
||||
New-Item -ItemType Directory -Path $backupDataPath -Force | Out-Null
|
||||
Copy-Item -Path $dbFile -Destination (Join-Path $backupDataPath "telegram_bot.db") -Force
|
||||
Write-Success "Database backed up"
|
||||
}
|
||||
|
||||
Write-Success "Backup created at: $backupFullPath"
|
||||
|
||||
# Clean old backups (keep last 10)
|
||||
$allBackups = Get-ChildItem -Path $Config.BackupPath -Directory |
|
||||
Where-Object { $_.Name -like "backup-*" } |
|
||||
Sort-Object Name -Descending
|
||||
|
||||
if ($allBackups.Count -gt 10) {
|
||||
$oldBackups = $allBackups | Select-Object -Skip 10
|
||||
foreach ($oldBackup in $oldBackups) {
|
||||
Remove-Item -Path $oldBackup.FullName -Recurse -Force
|
||||
Write-Success "Cleaned old backup: $($oldBackup.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
return $backupFullPath
|
||||
} catch {
|
||||
Write-Error "Backup failed: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-BotService {
|
||||
Write-Step "Stopping Telegram bot service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Warning "Service $($Config.ServiceName) not found"
|
||||
$DeploymentState.ServiceWasRunning = $false
|
||||
return
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
$DeploymentState.ServiceWasRunning = $true
|
||||
|
||||
Stop-Service -Name $Config.ServiceName -Force
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Wait for service to stop
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Success "Service stopped successfully"
|
||||
} else {
|
||||
Write-Warning "Service did not stop within timeout"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Service already stopped"
|
||||
$DeploymentState.ServiceWasRunning = $false
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to stop service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Update-ApplicationFiles {
|
||||
Write-Step "Updating application files..."
|
||||
|
||||
$sourceApp = Join-Path $Config.SourcePath "app"
|
||||
|
||||
if (-not (Test-Path $sourceApp)) {
|
||||
throw "Source app directory not found: $sourceApp"
|
||||
}
|
||||
|
||||
try {
|
||||
# Remove old app directory
|
||||
$destApp = Join-Path $Config.InstallPath "app"
|
||||
if (Test-Path $destApp) {
|
||||
Remove-Item -Path $destApp -Recurse -Force
|
||||
Write-Success "Removed old app directory"
|
||||
}
|
||||
|
||||
# Copy new app files
|
||||
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
|
||||
Write-Success "Application files updated"
|
||||
|
||||
# Update requirements.txt if present
|
||||
$sourceReq = Join-Path $Config.SourcePath "requirements.txt"
|
||||
$destReq = Join-Path $Config.InstallPath "requirements.txt"
|
||||
|
||||
if (Test-Path $sourceReq) {
|
||||
$sourceHash = (Get-FileHash $sourceReq -Algorithm SHA256).Hash
|
||||
$destHash = if (Test-Path $destReq) {
|
||||
(Get-FileHash $destReq -Algorithm SHA256).Hash
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if ($sourceHash -ne $destHash) {
|
||||
Write-Step "Requirements changed, updating Python dependencies..."
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
|
||||
# Use virtual environment pip
|
||||
$venvPath = Join-Path $Config.InstallPath "venv"
|
||||
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
|
||||
|
||||
if (Test-Path $pipPath) {
|
||||
try {
|
||||
& $pipPath install -r $destReq --upgrade
|
||||
Write-Success "Python dependencies updated"
|
||||
} catch {
|
||||
Write-Error "Failed to update Python dependencies: $_"
|
||||
throw
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Virtual environment not found, skipping dependency update"
|
||||
}
|
||||
} else {
|
||||
Write-Success "Python dependencies unchanged"
|
||||
}
|
||||
}
|
||||
|
||||
# Preserve .env file (never overwrite)
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
if (-not (Test-Path $envFile)) {
|
||||
$sourceEnv = Join-Path $Config.SourcePath ".env.example"
|
||||
if (Test-Path $sourceEnv) {
|
||||
Copy-Item -Path $sourceEnv -Destination $envFile -Force
|
||||
Write-Warning "Created .env from .env.example - PLEASE CONFIGURE"
|
||||
}
|
||||
} else {
|
||||
Write-Success ".env file preserved (not overwritten)"
|
||||
}
|
||||
|
||||
# Update management scripts
|
||||
$sourceScripts = Join-Path $Config.SourcePath "scripts"
|
||||
if (Test-Path $sourceScripts) {
|
||||
$destScripts = Join-Path $Config.InstallPath "scripts"
|
||||
if (-not (Test-Path $destScripts)) {
|
||||
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
|
||||
}
|
||||
|
||||
# List of management scripts to update
|
||||
$managementScripts = @(
|
||||
"Start-TelegramBot.ps1",
|
||||
"Stop-TelegramBot.ps1",
|
||||
"Restart-TelegramBot.ps1",
|
||||
"Backup-TelegramDB.ps1",
|
||||
"Setup-DailyBackup.ps1",
|
||||
"Setup-ClaudeAuth.ps1"
|
||||
)
|
||||
|
||||
$updatedScriptsCount = 0
|
||||
foreach ($script in $managementScripts) {
|
||||
$sourcePath = Join-Path $sourceScripts $script
|
||||
if (Test-Path $sourcePath) {
|
||||
$destPath = Join-Path $destScripts $script
|
||||
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||
$updatedScriptsCount++
|
||||
}
|
||||
}
|
||||
|
||||
if ($updatedScriptsCount -gt 0) {
|
||||
Write-Success "Updated $updatedScriptsCount management scripts"
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to update application files: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Start-BotService {
|
||||
if (-not $RestartService) {
|
||||
Write-Warning "Service restart disabled, skipping..."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Starting Telegram bot service..."
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $service) {
|
||||
Write-Warning "Service $($Config.ServiceName) not found"
|
||||
return
|
||||
}
|
||||
|
||||
Start-Service -Name $Config.ServiceName
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# Wait for service to start
|
||||
$timeout = 30
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Success "Service started successfully"
|
||||
} else {
|
||||
throw "Service failed to start (Status: $($service.Status))"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to start service: $_"
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DeploymentHealth {
|
||||
Write-Step "Testing deployment health..."
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
# Get service port from .env or use default
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
$port = 8002
|
||||
|
||||
if (Test-Path $envFile) {
|
||||
$envContent = Get-Content $envFile
|
||||
$portLine = $envContent | Where-Object { $_ -match "^INTERNAL_API_PORT=(\d+)" }
|
||||
if ($portLine -and $matches[1]) {
|
||||
$port = [int]$matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
$healthUrl = "http://localhost:$port/internal/health"
|
||||
$response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10
|
||||
|
||||
if ($response.StatusCode -eq 200) {
|
||||
$content = $response.Content | ConvertFrom-Json
|
||||
Write-Success "Health check passed: $($content.status)"
|
||||
Write-Success "Database: $($content.database.status)"
|
||||
return $true
|
||||
} else {
|
||||
throw "Health check returned status code: $($response.StatusCode)"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Health check failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Restore-FromBackup {
|
||||
param([string]$BackupPath)
|
||||
|
||||
if (-not $BackupPath -or -not (Test-Path $BackupPath)) {
|
||||
Write-Error "Cannot rollback: backup path not found ($BackupPath)"
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Step "Rolling back to backup: $BackupPath"
|
||||
|
||||
try {
|
||||
# Stop service
|
||||
Stop-BotService
|
||||
|
||||
# Restore app directory
|
||||
$backupApp = Join-Path $BackupPath "app"
|
||||
$destApp = Join-Path $Config.InstallPath "app"
|
||||
|
||||
if (Test-Path $backupApp) {
|
||||
if (Test-Path $destApp) {
|
||||
Remove-Item -Path $destApp -Recurse -Force
|
||||
}
|
||||
Copy-Item -Path $backupApp -Destination $destApp -Recurse -Force
|
||||
Write-Success "App files restored"
|
||||
}
|
||||
|
||||
# Restore requirements.txt
|
||||
$backupReq = Join-Path $BackupPath "requirements.txt"
|
||||
if (Test-Path $backupReq) {
|
||||
Copy-Item -Path $backupReq -Destination (Join-Path $Config.InstallPath "requirements.txt") -Force
|
||||
Write-Success "Requirements file restored"
|
||||
}
|
||||
|
||||
# Restore database
|
||||
$backupDb = Join-Path $BackupPath "data\telegram_bot.db"
|
||||
if (Test-Path $backupDb) {
|
||||
Copy-Item -Path $backupDb -Destination (Join-Path $Config.DataPath "telegram_bot.db") -Force
|
||||
Write-Success "Database restored"
|
||||
}
|
||||
|
||||
# Restart service
|
||||
if ($DeploymentState.ServiceWasRunning) {
|
||||
Start-BotService
|
||||
}
|
||||
|
||||
Write-Success "Rollback completed successfully"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Rollback failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-DeploymentSummary {
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
if ($DeploymentState.DeploymentSuccess) {
|
||||
Write-Host " DEPLOYMENT COMPLETED SUCCESSFULLY" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " DEPLOYMENT FAILED" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nDeployment Details:" -ForegroundColor Yellow
|
||||
Write-Host " Install Path: $($Config.InstallPath)"
|
||||
Write-Host " Source Path: $($Config.SourcePath)"
|
||||
Write-Host " Backup Created: $(if ($DeploymentState.BackupPath) { 'Yes' } else { 'No' })"
|
||||
|
||||
if ($DeploymentState.BackupPath) {
|
||||
Write-Host " Backup Location: $($DeploymentState.BackupPath)"
|
||||
}
|
||||
|
||||
$service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue
|
||||
if ($service) {
|
||||
Write-Host " Service Status: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" })
|
||||
}
|
||||
|
||||
if ($DeploymentState.DeploymentSuccess) {
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " - Monitor logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50 -Wait"
|
||||
Write-Host " - Check health: Invoke-WebRequest http://localhost:8002/internal/health"
|
||||
Write-Host " - Test bot on Telegram"
|
||||
} else {
|
||||
Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
|
||||
Write-Host " - Check logs: Get-Content $($Config.LogsPath)\stderr.log -Tail 100"
|
||||
Write-Host " - Verify .env configuration"
|
||||
if ($DeploymentState.BackupPath -and $RollbackOnFailure) {
|
||||
Write-Host " - Rollback completed automatically to: $($DeploymentState.BackupPath)"
|
||||
} elseif ($DeploymentState.BackupPath) {
|
||||
Write-Host " - Manual rollback available at: $($DeploymentState.BackupPath)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN DEPLOYMENT FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Deployment Script
|
||||
Quick deployment and update automation
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
if (-not (Test-Path $Config.InstallPath)) {
|
||||
Write-Error "Installation path not found: $($Config.InstallPath)"
|
||||
Write-Host " Run Install-TelegramBot.ps1 first" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Installation path verified"
|
||||
|
||||
if (-not (Test-Path $Config.SourcePath)) {
|
||||
Write-Error "Source path not found: $($Config.SourcePath)"
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Source path verified: $($Config.SourcePath)"
|
||||
|
||||
try {
|
||||
# Deployment steps
|
||||
$DeploymentState.BackupPath = Backup-CurrentDeployment
|
||||
Stop-BotService
|
||||
Update-ApplicationFiles
|
||||
Start-BotService
|
||||
|
||||
$healthOk = Test-DeploymentHealth
|
||||
|
||||
if ($healthOk) {
|
||||
$DeploymentState.DeploymentSuccess = $true
|
||||
Write-Host "`nDeployment completed successfully!" -ForegroundColor Green
|
||||
} else {
|
||||
throw "Health check failed after deployment"
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[DEPLOYMENT FAILED] $_" -ForegroundColor Red
|
||||
|
||||
if ($RollbackOnFailure -and $DeploymentState.BackupPath) {
|
||||
Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow
|
||||
$rollbackOk = Restore-FromBackup -BackupPath $DeploymentState.BackupPath
|
||||
|
||||
if ($rollbackOk) {
|
||||
Write-Host "Rollback completed successfully" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Rollback failed - manual intervention required" -ForegroundColor Red
|
||||
}
|
||||
} else {
|
||||
Write-Host "Automatic rollback disabled or no backup available" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$DeploymentState.DeploymentSuccess = $false
|
||||
} finally {
|
||||
Show-DeploymentSummary
|
||||
}
|
||||
|
||||
if (-not $DeploymentState.DeploymentSuccess) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main deployment
|
||||
Main
|
||||
382
deployment/windows/scripts/Enable-HTTPS.ps1
Normal file
382
deployment/windows/scripts/Enable-HTTPS.ps1
Normal file
@@ -0,0 +1,382 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enable HTTPS for ROA2WEB on IIS
|
||||
|
||||
.DESCRIPTION
|
||||
This script configures HTTPS for ROA2WEB by:
|
||||
- Creating a self-signed SSL certificate (or using existing certificate)
|
||||
- Configuring HTTPS binding on IIS site
|
||||
- Enabling HTTP to HTTPS redirect
|
||||
|
||||
.PARAMETER IISSiteName
|
||||
IIS Site name (default: Default Web Site)
|
||||
|
||||
.PARAMETER CertificateDnsName
|
||||
DNS name for certificate (default: server IP or hostname)
|
||||
|
||||
.PARAMETER UseExistingCert
|
||||
Use existing certificate by thumbprint
|
||||
|
||||
.PARAMETER CertThumbprint
|
||||
Thumbprint of existing certificate to use
|
||||
|
||||
.PARAMETER Port
|
||||
HTTPS port (default: 443)
|
||||
|
||||
.EXAMPLE
|
||||
.\Enable-HTTPS.ps1
|
||||
Create self-signed certificate and enable HTTPS
|
||||
|
||||
.EXAMPLE
|
||||
.\Enable-HTTPS.ps1 -IISSiteName "ROA2WEB" -CertificateDnsName "roa2web.company.com"
|
||||
Create certificate with custom DNS name
|
||||
|
||||
.EXAMPLE
|
||||
.\Enable-HTTPS.ps1 -UseExistingCert -CertThumbprint "ABC123..."
|
||||
Use existing certificate
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$IISSiteName = "Default Web Site",
|
||||
[string]$CertificateDnsName = "",
|
||||
[switch]$UseExistingCert,
|
||||
[string]$CertThumbprint = "",
|
||||
[int]$Port = 443,
|
||||
[string]$IPAddress = "*"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN SCRIPT
|
||||
# =============================================================================
|
||||
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Enable HTTPS on IIS
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check administrator privileges
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
# Import required modules
|
||||
Write-Step "Loading IIS module..."
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
Write-Success "IIS module loaded"
|
||||
|
||||
# Verify IIS site exists
|
||||
Write-Step "Verifying IIS site '$IISSiteName'..."
|
||||
$site = Get-Website -Name $IISSiteName -ErrorAction SilentlyContinue
|
||||
if (-not $site) {
|
||||
Write-Error "IIS site '$IISSiteName' not found"
|
||||
Write-Host "`nAvailable sites:" -ForegroundColor Yellow
|
||||
Get-Website | Format-Table Name, State, PhysicalPath
|
||||
exit 1
|
||||
}
|
||||
Write-Success "IIS site found: $IISSiteName (State: $($site.State))"
|
||||
|
||||
# Get or create certificate
|
||||
if ($UseExistingCert) {
|
||||
Write-Step "Using existing certificate..."
|
||||
|
||||
if ([string]::IsNullOrEmpty($CertThumbprint)) {
|
||||
Write-Host "`nAvailable certificates in LocalMachine\My:" -ForegroundColor Yellow
|
||||
Get-ChildItem -Path "cert:\LocalMachine\My" |
|
||||
Select-Object Thumbprint, Subject, FriendlyName, NotAfter |
|
||||
Format-Table -AutoSize
|
||||
|
||||
$CertThumbprint = Read-Host "Enter certificate thumbprint"
|
||||
}
|
||||
|
||||
$cert = Get-ChildItem -Path "cert:\LocalMachine\My\$CertThumbprint" -ErrorAction SilentlyContinue
|
||||
if (-not $cert) {
|
||||
Write-Error "Certificate with thumbprint '$CertThumbprint' not found"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Success "Certificate found: $($cert.Subject)"
|
||||
|
||||
} else {
|
||||
Write-Step "Creating self-signed SSL certificate..."
|
||||
|
||||
# Auto-detect DNS name if not provided
|
||||
if ([string]::IsNullOrEmpty($CertificateDnsName)) {
|
||||
$serverName = $env:COMPUTERNAME
|
||||
$ipAddress = (Get-NetIPAddress -AddressFamily IPv4 |
|
||||
Where-Object {$_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.*"} |
|
||||
Select-Object -First 1).IPAddress
|
||||
|
||||
Write-Host " Detected hostname: $serverName" -ForegroundColor Yellow
|
||||
Write-Host " Detected IP: $ipAddress" -ForegroundColor Yellow
|
||||
|
||||
$response = Read-Host "Use hostname for certificate? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
$CertificateDnsName = $serverName
|
||||
} else {
|
||||
$CertificateDnsName = $ipAddress
|
||||
}
|
||||
}
|
||||
|
||||
# Check if certificate already exists
|
||||
$existingCert = Get-ChildItem -Path "cert:\LocalMachine\My" |
|
||||
Where-Object {$_.DnsNameList -contains $CertificateDnsName} |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($existingCert) {
|
||||
Write-Warning "Certificate for '$CertificateDnsName' already exists"
|
||||
$response = Read-Host "Use existing certificate? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
$cert = $existingCert
|
||||
Write-Success "Using existing certificate"
|
||||
} else {
|
||||
Write-Warning "Removing existing certificate..."
|
||||
Remove-Item -Path "cert:\LocalMachine\My\$($existingCert.Thumbprint)" -Force
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $cert) {
|
||||
# Create new certificate
|
||||
$cert = New-SelfSignedCertificate `
|
||||
-DnsName $CertificateDnsName `
|
||||
-CertStoreLocation "cert:\LocalMachine\My" `
|
||||
-NotAfter (Get-Date).AddYears(5) `
|
||||
-FriendlyName "ROA2WEB SSL Certificate" `
|
||||
-KeyUsage DigitalSignature, KeyEncipherment `
|
||||
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")
|
||||
|
||||
Write-Success "Certificate created successfully"
|
||||
Write-Host " DNS Name: $CertificateDnsName" -ForegroundColor Yellow
|
||||
Write-Host " Thumbprint: $($cert.Thumbprint)" -ForegroundColor Yellow
|
||||
Write-Host " Valid Until: $($cert.NotAfter)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Configure HTTPS binding
|
||||
Write-Step "Configuring HTTPS binding..."
|
||||
|
||||
# Check if HTTPS binding already exists
|
||||
$existingBinding = Get-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingBinding) {
|
||||
Write-Warning "HTTPS binding already exists on port $Port"
|
||||
$response = Read-Host "Remove and recreate binding? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
Remove-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -ErrorAction SilentlyContinue
|
||||
Write-Success "Existing binding removed"
|
||||
} else {
|
||||
Write-Warning "Skipping binding creation"
|
||||
$existingBinding = $null
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $existingBinding) {
|
||||
# Create HTTPS binding
|
||||
try {
|
||||
New-WebBinding -Name $IISSiteName -Protocol "https" -Port $Port -IPAddress $IPAddress
|
||||
Write-Success "HTTPS binding created on port $Port"
|
||||
} catch {
|
||||
Write-Error "Failed to create HTTPS binding: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Attach certificate to binding
|
||||
Write-Step "Attaching certificate to HTTPS binding..."
|
||||
|
||||
try {
|
||||
# Method 1: Using IIS PowerShell Provider (IIS 8+)
|
||||
Push-Location
|
||||
Set-Location IIS:\SslBindings
|
||||
|
||||
# Remove existing binding if present
|
||||
$sslBinding = "${IPAddress}!${Port}"
|
||||
if ($IPAddress -eq "*") {
|
||||
$sslBinding = "0.0.0.0!${Port}"
|
||||
}
|
||||
|
||||
if (Test-Path $sslBinding) {
|
||||
Remove-Item $sslBinding -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Create new SSL binding
|
||||
$cert | New-Item $sslBinding
|
||||
Pop-Location
|
||||
|
||||
Write-Success "Certificate attached to HTTPS binding"
|
||||
|
||||
} catch {
|
||||
Write-Warning "Method 1 failed, trying alternative method..."
|
||||
|
||||
try {
|
||||
# Method 2: Using netsh (more compatible)
|
||||
$certHash = $cert.Thumbprint
|
||||
$appId = "{4dc3e181-e14b-4a21-b022-59fc669b0914}"
|
||||
|
||||
# Remove existing binding
|
||||
netsh http delete sslcert ipport=0.0.0.0:$Port 2>&1 | Out-Null
|
||||
|
||||
# Add new binding
|
||||
$result = netsh http add sslcert ipport=0.0.0.0:$Port certhash=$certHash appid=$appId
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Certificate attached using netsh"
|
||||
} else {
|
||||
Write-Error "Failed to attach certificate: $result"
|
||||
exit 1
|
||||
}
|
||||
|
||||
} catch {
|
||||
Write-Error "Failed to attach certificate: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Update web.config for HTTP to HTTPS redirect
|
||||
Write-Step "Checking web.config for HTTPS redirect..."
|
||||
|
||||
$webConfigPath = Join-Path $site.PhysicalPath "web.config"
|
||||
|
||||
if (Test-Path $webConfigPath) {
|
||||
$webConfig = Get-Content $webConfigPath -Raw
|
||||
|
||||
if ($webConfig -match "Force HTTPS") {
|
||||
Write-Success "HTTPS redirect rule already exists in web.config"
|
||||
} else {
|
||||
Write-Warning "HTTPS redirect rule not found in web.config"
|
||||
Write-Host "`nTo enable automatic HTTP to HTTPS redirect, add this rule to web.config:" -ForegroundColor Yellow
|
||||
Write-Host @"
|
||||
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
"@ -ForegroundColor Gray
|
||||
|
||||
$response = Read-Host "`nAdd this rule automatically? (Y/n)"
|
||||
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
|
||||
# Backup web.config
|
||||
$backupPath = "$webConfigPath.backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||
Copy-Item -Path $webConfigPath -Destination $backupPath
|
||||
Write-Success "web.config backed up to: $backupPath"
|
||||
|
||||
# Add redirect rule
|
||||
$redirectRule = @"
|
||||
<!-- Rule: Force HTTPS (redirect HTTP to HTTPS) -->
|
||||
<rule name="Force HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
|
||||
</rule>
|
||||
|
||||
"@
|
||||
|
||||
$webConfig = $webConfig -replace '(<rules>)', "`$1`r`n$redirectRule"
|
||||
Set-Content -Path $webConfigPath -Value $webConfig
|
||||
Write-Success "HTTPS redirect rule added to web.config"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Warning "web.config not found at: $webConfigPath"
|
||||
Write-Host " Please ensure web.config exists and contains proper rewrite rules" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Test configuration
|
||||
Write-Step "Testing configuration..."
|
||||
|
||||
# Restart IIS site to apply changes
|
||||
try {
|
||||
Stop-Website -Name $IISSiteName -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
Start-Website -Name $IISSiteName
|
||||
Write-Success "IIS site restarted"
|
||||
} catch {
|
||||
Write-Warning "Could not restart IIS site: $_"
|
||||
}
|
||||
|
||||
# Display summary
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
Write-Host " HTTPS CONFIGURATION COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 70) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nConfiguration Summary:" -ForegroundColor Yellow
|
||||
Write-Host " IIS Site: $IISSiteName"
|
||||
Write-Host " HTTPS Port: $Port"
|
||||
Write-Host " Certificate: $($cert.Subject)"
|
||||
Write-Host " Thumbprint: $($cert.Thumbprint)"
|
||||
Write-Host " Valid Until: $($cert.NotAfter)"
|
||||
|
||||
Write-Host "`nAccess Points:" -ForegroundColor Yellow
|
||||
Write-Host " HTTPS URL: https://$CertificateDnsName"
|
||||
if ($site.Bindings.Collection | Where-Object {$_.protocol -eq "http"}) {
|
||||
Write-Host " HTTP URL: http://$CertificateDnsName (will redirect to HTTPS)"
|
||||
}
|
||||
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
|
||||
if ($cert.Subject -match "CN=[\d\.]+") {
|
||||
Write-Host " 1. [RECOMMENDED] Replace self-signed certificate with CA-issued certificate"
|
||||
Write-Host " - Browsers will show security warnings for self-signed certificates"
|
||||
Write-Host " - Use Let's Encrypt, DigiCert, or another CA for production"
|
||||
}
|
||||
|
||||
Write-Host " 2. Test HTTPS access: https://$CertificateDnsName"
|
||||
Write-Host " 3. Verify HTTP to HTTPS redirect is working"
|
||||
Write-Host " 4. Check browser console for mixed content warnings"
|
||||
|
||||
Write-Host "`nTroubleshooting:" -ForegroundColor Yellow
|
||||
Write-Host " View IIS bindings: Get-WebBinding -Name '$IISSiteName'"
|
||||
Write-Host " View certificates: Get-ChildItem cert:\LocalMachine\My"
|
||||
Write-Host " Test HTTPS locally: Invoke-WebRequest https://localhost:$Port -SkipCertificateCheck"
|
||||
Write-Host " IIS Manager: inetmgr"
|
||||
|
||||
Write-Host "`n" + ("=" * 70) -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
598
deployment/windows/scripts/Install-ROA2WEB.ps1
Normal file
598
deployment/windows/scripts/Install-ROA2WEB.ps1
Normal file
@@ -0,0 +1,598 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB - Initial Installation Script for Windows Server + IIS
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs complete installation of ROA2WEB on Windows Server:
|
||||
- Checks prerequisites (Admin rights, IIS)
|
||||
- Installs Python 3.11+ if needed
|
||||
- Installs NSSM (service manager)
|
||||
- Installs IIS URL Rewrite and ARR modules
|
||||
- Creates directory structure
|
||||
- Installs Python dependencies
|
||||
- Creates Windows Service for backend
|
||||
- Configures IIS website
|
||||
- Starts all services
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web)
|
||||
|
||||
.PARAMETER PythonVersion
|
||||
Python version to install (default: 3.11.9)
|
||||
|
||||
.PARAMETER ServicePort
|
||||
Backend service port (default: 8000)
|
||||
|
||||
.PARAMETER SkipPython
|
||||
Skip Python installation (use existing Python)
|
||||
|
||||
.PARAMETER SkipIIS
|
||||
Skip IIS configuration
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-ROA2WEB.ps1
|
||||
Standard installation with defaults
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-ROA2WEB.ps1 -InstallPath "D:\Apps\roa2web" -ServicePort 8001
|
||||
Custom installation path and port
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web",
|
||||
[string]$PythonVersion = "3.11.9",
|
||||
[int]$ServicePort = 8000,
|
||||
[string]$IISSiteName = "Default Web Site",
|
||||
[string]$IISAppName = "roa2web",
|
||||
[switch]$CreateNewSite,
|
||||
[switch]$SkipPython,
|
||||
[switch]$SkipIIS
|
||||
)
|
||||
|
||||
# Strict error handling
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB"
|
||||
ServiceName = "ROA2WEB-Backend"
|
||||
ServiceDisplayName = "ROA2WEB Backend Service"
|
||||
ServiceDescription = "FastAPI backend service for ROA2WEB ERP Reports Application"
|
||||
InstallPath = $InstallPath
|
||||
BackendPath = Join-Path $InstallPath "backend"
|
||||
FrontendPath = Join-Path $InstallPath "frontend"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
TempPath = Join-Path $InstallPath "temp"
|
||||
PythonVersion = $PythonVersion
|
||||
ServicePort = $ServicePort
|
||||
IISSiteName = $IISSiteName
|
||||
IISAppName = $IISAppName
|
||||
IISAppPoolName = "ROA2WEB-AppPool"
|
||||
CreateNewSite = $CreateNewSite
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-CommandExists {
|
||||
param([string]$Command)
|
||||
try {
|
||||
if (Get-Command $Command -ErrorAction Stop) {
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Chocolatey {
|
||||
Write-Step "Installing Chocolatey package manager..."
|
||||
|
||||
if (Test-CommandExists "choco") {
|
||||
Write-Success "Chocolatey already installed"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
|
||||
# Refresh environment
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
Write-Success "Chocolatey installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Chocolatey: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-Python {
|
||||
Write-Step "Checking Python installation..."
|
||||
|
||||
if ($SkipPython) {
|
||||
Write-Warning "Skipping Python installation (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
# Check if Python is already installed
|
||||
try {
|
||||
$pythonCmd = Get-Command python -ErrorAction Stop
|
||||
$pythonVersionOutput = & python --version 2>&1
|
||||
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
|
||||
$installedVersion = $matches[1]
|
||||
Write-Success "Python $installedVersion already installed at $($pythonCmd.Source)"
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Python not found, will install..."
|
||||
}
|
||||
|
||||
# Install Python via Chocolatey
|
||||
Write-Step "Installing Python $PythonVersion..."
|
||||
try {
|
||||
choco install python --version=$PythonVersion -y --force
|
||||
|
||||
# Refresh environment
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
Write-Success "Python $PythonVersion installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Python: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-NSSM {
|
||||
Write-Step "Installing NSSM (service manager)..."
|
||||
|
||||
if (Test-Path "C:\nssm\nssm.exe") {
|
||||
Write-Success "NSSM already installed"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
choco install nssm -y
|
||||
Write-Success "NSSM installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install NSSM: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-IISModules {
|
||||
if ($SkipIIS) {
|
||||
Write-Warning "Skipping IIS configuration (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Checking IIS installation..."
|
||||
|
||||
# Detect OS type (Server vs Desktop)
|
||||
$osInfo = Get-CimInstance -ClassName Win32_OperatingSystem
|
||||
$isServer = $osInfo.ProductType -eq 3 # 1=Workstation, 2=Domain Controller, 3=Server
|
||||
|
||||
# Check if IIS is installed (different cmdlets for Server vs Desktop)
|
||||
$iisInstalled = $false
|
||||
|
||||
if ($isServer) {
|
||||
# Windows Server - use Get-WindowsFeature
|
||||
$iisFeature = Get-WindowsFeature -Name Web-Server -ErrorAction SilentlyContinue
|
||||
$iisInstalled = $iisFeature -and $iisFeature.InstallState -eq "Installed"
|
||||
|
||||
if (-not $iisInstalled) {
|
||||
Write-Error "IIS is not installed. Please install IIS first:"
|
||||
Write-Host " Install-WindowsFeature -Name Web-Server -IncludeManagementTools" -ForegroundColor Yellow
|
||||
throw "IIS not installed"
|
||||
}
|
||||
} else {
|
||||
# Windows Desktop (10/11) - use Get-WindowsOptionalFeature
|
||||
$iisFeature = Get-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -ErrorAction SilentlyContinue
|
||||
$iisInstalled = $iisFeature -and $iisFeature.State -eq "Enabled"
|
||||
|
||||
if (-not $iisInstalled) {
|
||||
Write-Error "IIS is not installed. Please install IIS first:"
|
||||
Write-Host " Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer -All" -ForegroundColor Yellow
|
||||
Write-Host " Or use: Control Panel -> Programs -> Turn Windows features on/off -> Internet Information Services" -ForegroundColor Yellow
|
||||
throw "IIS not installed"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Success "IIS is installed ($($osInfo.Caption))"
|
||||
|
||||
# Install URL Rewrite Module
|
||||
Write-Step "Installing IIS URL Rewrite Module..."
|
||||
$urlRewriteInstalled = Get-WebConfiguration -Filter "/system.webServer/rewrite" -PSPath "IIS:\" -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $urlRewriteInstalled) {
|
||||
Write-Warning "URL Rewrite not found, installing..."
|
||||
try {
|
||||
$urlRewriteUrl = "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi"
|
||||
$urlRewritePath = "$env:TEMP\rewrite_amd64.msi"
|
||||
|
||||
Invoke-WebRequest -Uri $urlRewriteUrl -OutFile $urlRewritePath
|
||||
Start-Process msiexec.exe -ArgumentList "/i", $urlRewritePath, "/quiet", "/norestart" -Wait
|
||||
Remove-Item $urlRewritePath -Force
|
||||
|
||||
Write-Success "URL Rewrite Module installed"
|
||||
} catch {
|
||||
Write-Error "Failed to install URL Rewrite: $_"
|
||||
Write-Warning "You can download it manually from: https://www.iis.net/downloads/microsoft/url-rewrite"
|
||||
}
|
||||
} else {
|
||||
Write-Success "URL Rewrite Module already installed"
|
||||
}
|
||||
|
||||
# Install Application Request Routing (ARR)
|
||||
Write-Step "Checking Application Request Routing (ARR)..."
|
||||
try {
|
||||
choco install iis-arr -y
|
||||
Write-Success "ARR installed successfully"
|
||||
} catch {
|
||||
Write-Warning "Could not install ARR via Chocolatey. Download manually from: https://www.iis.net/downloads/microsoft/application-request-routing"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DirectoryStructure {
|
||||
Write-Step "Creating directory structure..."
|
||||
|
||||
$directories = @(
|
||||
$Config.InstallPath,
|
||||
$Config.BackendPath,
|
||||
$Config.FrontendPath,
|
||||
$Config.LogsPath,
|
||||
$Config.TempPath,
|
||||
(Join-Path $Config.BackendPath "logs"),
|
||||
(Join-Path $Config.BackendPath "temp")
|
||||
)
|
||||
|
||||
foreach ($dir in $directories) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
Write-Success "Created: $dir"
|
||||
} else {
|
||||
Write-Success "Already exists: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Set permissions (IIS user needs read access)
|
||||
try {
|
||||
$acl = Get-Acl $Config.InstallPath
|
||||
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("IIS_IUSRS", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
|
||||
$acl.SetAccessRule($accessRule)
|
||||
Set-Acl -Path $Config.InstallPath -AclObject $acl
|
||||
Write-Success "Permissions set for IIS_IUSRS"
|
||||
} catch {
|
||||
Write-Warning "Could not set permissions: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PythonDependencies {
|
||||
Write-Step "Installing Python dependencies..."
|
||||
|
||||
$requirementsPath = Join-Path $Config.BackendPath "requirements.txt"
|
||||
|
||||
if (-not (Test-Path $requirementsPath)) {
|
||||
Write-Warning "requirements.txt not found at $requirementsPath"
|
||||
Write-Warning "Please copy backend files first, then run this script again"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Upgrade pip first
|
||||
& python -m pip install --upgrade pip
|
||||
|
||||
# Install dependencies
|
||||
& python -m pip install -r $requirementsPath
|
||||
|
||||
Write-Success "Python dependencies installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Python dependencies: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-WindowsService {
|
||||
Write-Step "Creating Windows Service for backend..."
|
||||
|
||||
# Check if service already exists using nssm (more reliable than Get-Service)
|
||||
# Temporarily disable error action to check service status
|
||||
$oldErrorAction = $ErrorActionPreference
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
$nssmOutput = & nssm status $Config.ServiceName 2>&1
|
||||
$serviceExists = $LASTEXITCODE -eq 0
|
||||
|
||||
$ErrorActionPreference = $oldErrorAction
|
||||
|
||||
if ($serviceExists) {
|
||||
Write-Warning "Service already exists, stopping and removing..."
|
||||
# Try to stop service (may fail if service is broken)
|
||||
& nssm stop $Config.ServiceName 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
# Force remove service
|
||||
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Success "Existing service removed"
|
||||
}
|
||||
|
||||
# Get Python path
|
||||
$pythonPath = (Get-Command python).Source
|
||||
$uvicornModule = "uvicorn"
|
||||
$appModule = "app.main:app"
|
||||
|
||||
# NSSM service creation
|
||||
try {
|
||||
# Install service
|
||||
& nssm install $Config.ServiceName $pythonPath "-m" $uvicornModule $appModule "--host" "127.0.0.1" "--port" $Config.ServicePort.ToString() "--workers" "4"
|
||||
|
||||
# Set service configuration
|
||||
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
|
||||
& nssm set $Config.ServiceName Description $Config.ServiceDescription
|
||||
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
|
||||
& nssm set $Config.ServiceName AppDirectory $Config.BackendPath
|
||||
|
||||
# Set environment variables (PYTHONPATH for shared modules)
|
||||
# Point to the installation root so shared/ modules can be imported
|
||||
$pythonPath = $Config.InstallPath
|
||||
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$pythonPath"
|
||||
|
||||
# Set logging
|
||||
$stdoutLog = Join-Path $Config.LogsPath "backend-stdout.log"
|
||||
$stderrLog = Join-Path $Config.LogsPath "backend-stderr.log"
|
||||
& nssm set $Config.ServiceName AppStdout $stdoutLog
|
||||
& nssm set $Config.ServiceName AppStderr $stderrLog
|
||||
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
|
||||
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
|
||||
|
||||
# Set restart policy
|
||||
& nssm set $Config.ServiceName AppExit Default Restart
|
||||
& nssm set $Config.ServiceName AppRestartDelay 5000
|
||||
|
||||
Write-Success "Windows Service created successfully"
|
||||
} catch {
|
||||
throw "Failed to create Windows Service: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-IISWebsite {
|
||||
if ($SkipIIS) {
|
||||
Write-Warning "Skipping IIS website configuration (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Step "Configuring IIS application..."
|
||||
|
||||
Import-Module WebAdministration -ErrorAction Stop
|
||||
|
||||
# Remove existing app pool if present
|
||||
if (Test-Path "IIS:\AppPools\$($Config.IISAppPoolName)") {
|
||||
Write-Warning "Removing existing app pool..."
|
||||
Remove-WebAppPool -Name $Config.IISAppPoolName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Create Application Pool
|
||||
Write-Step "Creating IIS Application Pool..."
|
||||
New-WebAppPool -Name $Config.IISAppPoolName -Force | Out-Null
|
||||
Set-ItemProperty -Path "IIS:\AppPools\$($Config.IISAppPoolName)" -Name "managedRuntimeVersion" -Value ""
|
||||
Write-Success "Application Pool created: $($Config.IISAppPoolName)"
|
||||
|
||||
if ($CreateNewSite) {
|
||||
# Create new website (old behavior)
|
||||
Write-Step "Creating new IIS Website..."
|
||||
|
||||
# Stop default website if running
|
||||
try {
|
||||
Stop-Website -Name "Default Web Site" -ErrorAction SilentlyContinue
|
||||
Write-Success "Stopped Default Web Site"
|
||||
} catch {
|
||||
Write-Warning "Could not stop Default Web Site: $_"
|
||||
}
|
||||
|
||||
# Remove existing site if present
|
||||
if (Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue) {
|
||||
Write-Warning "Removing existing website..."
|
||||
Remove-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
New-Website -Name $Config.IISSiteName `
|
||||
-PhysicalPath $Config.FrontendPath `
|
||||
-ApplicationPool $Config.IISAppPoolName `
|
||||
-Port 80 `
|
||||
-Force | Out-Null
|
||||
|
||||
Write-Success "Website created: $($Config.IISSiteName)"
|
||||
|
||||
# Start website
|
||||
Start-Website -Name $Config.IISSiteName
|
||||
Write-Success "Website started: $($Config.IISSiteName)"
|
||||
} else {
|
||||
# Create application under existing site (default behavior)
|
||||
Write-Step "Creating IIS Application under '$($Config.IISSiteName)'..."
|
||||
|
||||
# Verify parent site exists
|
||||
$parentSite = Get-Website -Name $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
if (-not $parentSite) {
|
||||
throw "Parent website '$($Config.IISSiteName)' does not exist. Use -CreateNewSite to create a new site."
|
||||
}
|
||||
|
||||
# Remove existing application if present
|
||||
$existingApp = Get-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
if ($existingApp) {
|
||||
Write-Warning "Removing existing application..."
|
||||
Remove-WebApplication -Name $Config.IISAppName -Site $Config.IISSiteName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Create application
|
||||
New-WebApplication -Name $Config.IISAppName `
|
||||
-Site $Config.IISSiteName `
|
||||
-PhysicalPath $Config.FrontendPath `
|
||||
-ApplicationPool $Config.IISAppPoolName `
|
||||
-Force | Out-Null
|
||||
|
||||
Write-Success "Application created: /$($Config.IISAppName) under $($Config.IISSiteName)"
|
||||
}
|
||||
|
||||
# Copy web.config to frontend path
|
||||
$webConfigSource = Join-Path $PSScriptRoot "..\config\web.config"
|
||||
$webConfigDest = Join-Path $Config.FrontendPath "web.config"
|
||||
|
||||
if (Test-Path $webConfigSource) {
|
||||
Copy-Item -Path $webConfigSource -Destination $webConfigDest -Force
|
||||
Write-Success "web.config copied to frontend path"
|
||||
} else {
|
||||
Write-Warning "web.config not found at $webConfigSource"
|
||||
}
|
||||
}
|
||||
|
||||
function Start-Services {
|
||||
Write-Step "Starting services..."
|
||||
|
||||
# Start backend service
|
||||
try {
|
||||
Start-Service -Name $Config.ServiceName
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$service = Get-Service -Name $Config.ServiceName
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Success "Backend service started successfully"
|
||||
} else {
|
||||
Write-Error "Backend service failed to start (Status: $($service.Status))"
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Failed to start backend service: $_"
|
||||
}
|
||||
|
||||
# Test backend health
|
||||
Write-Step "Testing backend health..."
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/health" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Success "Backend health check passed"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Backend health check failed (may need time to start): $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Summary {
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " ROA2WEB INSTALLATION COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
|
||||
Write-Host " Install Path: $($Config.InstallPath)"
|
||||
Write-Host " Backend Path: $($Config.BackendPath)"
|
||||
Write-Host " Frontend Path: $($Config.FrontendPath)"
|
||||
Write-Host " Service Name: $($Config.ServiceName)"
|
||||
Write-Host " Service Port: $($Config.ServicePort)"
|
||||
Write-Host " IIS Site: $($Config.IISSiteName)"
|
||||
|
||||
Write-Host "`nAccess Points:" -ForegroundColor Yellow
|
||||
if ($Config.CreateNewSite) {
|
||||
Write-Host " Web Application: http://localhost"
|
||||
} else {
|
||||
Write-Host " Web Application: http://localhost/$($Config.IISAppName)"
|
||||
}
|
||||
Write-Host " API Backend: http://localhost:$($Config.ServicePort)"
|
||||
Write-Host " API Docs: http://localhost:$($Config.ServicePort)/docs"
|
||||
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/health"
|
||||
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Copy backend files to: $($Config.BackendPath)"
|
||||
Write-Host " 2. Copy frontend files to: $($Config.FrontendPath)"
|
||||
Write-Host " 3. Configure .env file at: $($Config.BackendPath)\.env"
|
||||
Write-Host " 4. Run: .\Deploy-ROA2WEB.ps1 to deploy updates"
|
||||
|
||||
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
|
||||
Write-Host " Start Service: .\Start-ROA2WEB.ps1"
|
||||
Write-Host " Stop Service: .\Stop-ROA2WEB.ps1"
|
||||
Write-Host " Restart Service: .\Restart-ROA2WEB.ps1"
|
||||
Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN INSTALLATION FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB - Windows Server Installation Script
|
||||
Modern ERP Reports Application with FastAPI + Vue.js + IIS
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
try {
|
||||
# Installation steps
|
||||
Install-Chocolatey
|
||||
Install-Python
|
||||
Install-NSSM
|
||||
Install-IISModules
|
||||
New-DirectoryStructure
|
||||
Install-PythonDependencies
|
||||
New-WindowsService
|
||||
Initialize-IISWebsite
|
||||
Start-Services
|
||||
Show-Summary
|
||||
|
||||
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main installation
|
||||
Main
|
||||
633
deployment/windows/scripts/Install-TelegramBot.ps1
Normal file
633
deployment/windows/scripts/Install-TelegramBot.ps1
Normal file
@@ -0,0 +1,633 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
ROA2WEB Telegram Bot - Installation Script for Windows Server
|
||||
|
||||
.DESCRIPTION
|
||||
This script performs complete installation of ROA2WEB Telegram Bot on Windows Server:
|
||||
- Checks prerequisites (Admin rights, Python)
|
||||
- Installs NSSM (service manager) if needed
|
||||
- Creates directory structure
|
||||
- Installs Python dependencies
|
||||
- Creates Windows Service for Telegram bot
|
||||
- Configures internal API
|
||||
- Starts service
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER ServicePort
|
||||
Internal API service port (default: 8002)
|
||||
|
||||
.PARAMETER SourcePath
|
||||
Source path for deployment package (auto-detected if run from scripts/ directory)
|
||||
|
||||
.PARAMETER SkipPython
|
||||
Skip Python installation check (use existing Python)
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-TelegramBot.ps1
|
||||
Standard installation with defaults (auto-detects source from scripts/ directory)
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-TelegramBot.ps1 -InstallPath "D:\Apps\roa2web\telegram-bot" -ServicePort 8003
|
||||
Custom installation path and port
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-TelegramBot.ps1 -SourcePath "C:\Deploy\telegram-bot"
|
||||
Install from specific source path
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges, Python 3.11+
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[int]$ServicePort = 8002,
|
||||
[string]$SourcePath = "",
|
||||
[switch]$SkipPython
|
||||
)
|
||||
|
||||
# Strict error handling
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect source path: if running from scripts/ subdirectory, use parent
|
||||
$detectedSourcePath = if ($SourcePath) {
|
||||
$SourcePath
|
||||
} elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") {
|
||||
Split-Path $PSScriptRoot -Parent
|
||||
} else {
|
||||
$PSScriptRoot
|
||||
}
|
||||
|
||||
$script:Config = @{
|
||||
AppName = "ROA2WEB-TelegramBot"
|
||||
ServiceName = "ROA2WEB-TelegramBot"
|
||||
ServiceDisplayName = "ROA2WEB Telegram Bot Service"
|
||||
ServiceDescription = "Telegram bot frontend for ROA2WEB with Claude Agent SDK"
|
||||
InstallPath = $InstallPath
|
||||
DataPath = Join-Path $InstallPath "data"
|
||||
LogsPath = Join-Path $InstallPath "logs"
|
||||
TempPath = Join-Path $InstallPath "temp"
|
||||
BackupPath = Join-Path $InstallPath "backups"
|
||||
ServicePort = $ServicePort
|
||||
SourcePath = $detectedSourcePath
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-CommandExists {
|
||||
param([string]$Command)
|
||||
try {
|
||||
if (Get-Command $Command -ErrorAction Stop) {
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PythonInstallation {
|
||||
Write-Step "Checking Python installation..."
|
||||
|
||||
if ($SkipPython) {
|
||||
Write-Warning "Skipping Python check (as requested)"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$pythonCmd = Get-Command python -ErrorAction Stop
|
||||
$pythonVersionOutput = & python --version 2>&1
|
||||
if ($pythonVersionOutput -match "Python (\d+\.\d+\.\d+)") {
|
||||
$installedVersion = $matches[1]
|
||||
$versionParts = $installedVersion -split '\.'
|
||||
$major = [int]$versionParts[0]
|
||||
$minor = [int]$versionParts[1]
|
||||
|
||||
if ($major -ge 3 -and $minor -ge 11) {
|
||||
Write-Success "Python $installedVersion found at $($pythonCmd.Source)"
|
||||
return
|
||||
} else {
|
||||
throw "Python 3.11+ required, found $installedVersion"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Python 3.11+ not found. Please install Python first."
|
||||
Write-Host " Download from: https://www.python.org/downloads/" -ForegroundColor Yellow
|
||||
throw "Python not installed"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-ApplicationFiles {
|
||||
Write-Step "Copying application files from deployment package..."
|
||||
|
||||
$sourceApp = Join-Path $Config.SourcePath "app"
|
||||
$sourceReq = Join-Path $Config.SourcePath "requirements.txt"
|
||||
$sourceScripts = Join-Path $Config.SourcePath "scripts"
|
||||
|
||||
# Validate source files exist
|
||||
if (-not (Test-Path $sourceApp)) {
|
||||
Write-Warning "Source app directory not found: $sourceApp"
|
||||
Write-Warning "You may need to copy application files manually"
|
||||
return $false
|
||||
}
|
||||
|
||||
if (-not (Test-Path $sourceReq)) {
|
||||
Write-Warning "Source requirements.txt not found: $sourceReq"
|
||||
Write-Warning "You may need to copy requirements.txt manually"
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
# Copy app directory
|
||||
$destApp = Join-Path $Config.InstallPath "app"
|
||||
if (Test-Path $destApp) {
|
||||
Write-Warning "App directory already exists, removing..."
|
||||
Remove-Item -Path $destApp -Recurse -Force
|
||||
}
|
||||
|
||||
Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force
|
||||
Write-Success "Application files copied"
|
||||
|
||||
# Copy requirements.txt
|
||||
$destReq = Join-Path $Config.InstallPath "requirements.txt"
|
||||
Copy-Item -Path $sourceReq -Destination $destReq -Force
|
||||
Write-Success "requirements.txt copied"
|
||||
|
||||
# Copy .env.example if exists (but don't overwrite .env)
|
||||
$sourceEnvExample = Join-Path $Config.SourcePath ".env.example"
|
||||
if (Test-Path $sourceEnvExample) {
|
||||
$destEnvExample = Join-Path $Config.InstallPath ".env.example"
|
||||
Copy-Item -Path $sourceEnvExample -Destination $destEnvExample -Force
|
||||
Write-Success ".env.example copied"
|
||||
}
|
||||
|
||||
# Copy management scripts (but exclude installation/deployment scripts)
|
||||
if (Test-Path $sourceScripts) {
|
||||
$destScripts = Join-Path $Config.InstallPath "scripts"
|
||||
if (-not (Test-Path $destScripts)) {
|
||||
New-Item -ItemType Directory -Path $destScripts -Force | Out-Null
|
||||
}
|
||||
|
||||
# List of management scripts to copy
|
||||
$managementScripts = @(
|
||||
"Start-TelegramBot.ps1",
|
||||
"Stop-TelegramBot.ps1",
|
||||
"Restart-TelegramBot.ps1",
|
||||
"Backup-TelegramDB.ps1",
|
||||
"Setup-DailyBackup.ps1",
|
||||
"Setup-ClaudeAuth.ps1"
|
||||
)
|
||||
|
||||
$copiedScriptsCount = 0
|
||||
foreach ($script in $managementScripts) {
|
||||
$sourcePath = Join-Path $sourceScripts $script
|
||||
if (Test-Path $sourcePath) {
|
||||
$destPath = Join-Path $destScripts $script
|
||||
Copy-Item -Path $sourcePath -Destination $destPath -Force
|
||||
$copiedScriptsCount++
|
||||
}
|
||||
}
|
||||
|
||||
if ($copiedScriptsCount -gt 0) {
|
||||
Write-Success "Copied $copiedScriptsCount management scripts to scripts/ directory"
|
||||
} else {
|
||||
Write-Warning "No management scripts found to copy"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Source scripts directory not found: $sourceScripts"
|
||||
}
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Failed to copy application files: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Install-NSSM {
|
||||
Write-Step "Installing NSSM (service manager)..."
|
||||
|
||||
if (Test-Path "C:\nssm\nssm.exe") {
|
||||
Write-Success "NSSM already installed"
|
||||
return
|
||||
}
|
||||
|
||||
# Check if Chocolatey is available
|
||||
if (Test-CommandExists "choco") {
|
||||
try {
|
||||
choco install nssm -y
|
||||
Write-Success "NSSM installed via Chocolatey"
|
||||
return
|
||||
} catch {
|
||||
Write-Warning "Chocolatey installation failed, trying direct download..."
|
||||
}
|
||||
}
|
||||
|
||||
# Direct download as fallback
|
||||
try {
|
||||
$nssmUrl = "https://nssm.cc/release/nssm-2.24.zip"
|
||||
$nssmZip = "$env:TEMP\nssm.zip"
|
||||
$nssmExtract = "$env:TEMP\nssm"
|
||||
|
||||
Write-Step "Downloading NSSM..."
|
||||
Invoke-WebRequest -Uri $nssmUrl -OutFile $nssmZip
|
||||
|
||||
Write-Step "Extracting NSSM..."
|
||||
Expand-Archive -Path $nssmZip -DestinationPath $nssmExtract -Force
|
||||
|
||||
# Copy nssm.exe to C:\nssm
|
||||
New-Item -ItemType Directory -Path "C:\nssm" -Force | Out-Null
|
||||
Copy-Item -Path "$nssmExtract\nssm-2.24\win64\nssm.exe" -Destination "C:\nssm\nssm.exe" -Force
|
||||
|
||||
# Add to PATH
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
if ($currentPath -notlike "*C:\nssm*") {
|
||||
[Environment]::SetEnvironmentVariable("Path", "$currentPath;C:\nssm", "Machine")
|
||||
$env:Path += ";C:\nssm"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
Remove-Item $nssmZip -Force
|
||||
Remove-Item $nssmExtract -Recurse -Force
|
||||
|
||||
Write-Success "NSSM installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install NSSM: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-DirectoryStructure {
|
||||
Write-Step "Creating directory structure..."
|
||||
|
||||
$directories = @(
|
||||
$Config.InstallPath,
|
||||
(Join-Path $Config.InstallPath "app"),
|
||||
$Config.DataPath,
|
||||
$Config.LogsPath,
|
||||
$Config.TempPath,
|
||||
$Config.BackupPath
|
||||
)
|
||||
|
||||
foreach ($dir in $directories) {
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
Write-Success "Created: $dir"
|
||||
} else {
|
||||
Write-Success "Already exists: $dir"
|
||||
}
|
||||
}
|
||||
|
||||
# Set permissions (service needs full access to data, logs, backups)
|
||||
try {
|
||||
$acl = Get-Acl $Config.InstallPath
|
||||
|
||||
# Grant SYSTEM full control
|
||||
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||
"SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
|
||||
)
|
||||
$acl.SetAccessRule($systemRule)
|
||||
|
||||
# Grant Administrators full control
|
||||
$adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||
"Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
|
||||
)
|
||||
$acl.SetAccessRule($adminRule)
|
||||
|
||||
Set-Acl -Path $Config.InstallPath -AclObject $acl
|
||||
Write-Success "Permissions set for SYSTEM and Administrators"
|
||||
} catch {
|
||||
Write-Warning "Could not set permissions: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PythonDependencies {
|
||||
Write-Step "Installing Python dependencies..."
|
||||
|
||||
$requirementsPath = Join-Path $Config.InstallPath "requirements.txt"
|
||||
|
||||
if (-not (Test-Path $requirementsPath)) {
|
||||
Write-Warning "requirements.txt not found at $requirementsPath"
|
||||
Write-Warning "Please copy application files first, then run: pip install -r requirements.txt"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Create virtual environment
|
||||
$venvPath = Join-Path $Config.InstallPath "venv"
|
||||
|
||||
if (-not (Test-Path $venvPath)) {
|
||||
Write-Step "Creating virtual environment..."
|
||||
& python -m venv $venvPath
|
||||
Write-Success "Virtual environment created"
|
||||
} else {
|
||||
Write-Success "Virtual environment already exists"
|
||||
}
|
||||
|
||||
# Activate and install dependencies
|
||||
$pipPath = Join-Path $venvPath "Scripts\pip.exe"
|
||||
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
|
||||
|
||||
Write-Step "Upgrading pip..."
|
||||
& $pythonPath -m pip install --upgrade pip
|
||||
|
||||
Write-Step "Installing dependencies..."
|
||||
& $pipPath install -r $requirementsPath
|
||||
|
||||
Write-Success "Python dependencies installed successfully"
|
||||
} catch {
|
||||
throw "Failed to install Python dependencies: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-WindowsService {
|
||||
Write-Step "Creating Windows Service for Telegram bot..."
|
||||
|
||||
# Check if service already exists
|
||||
$oldErrorAction = $ErrorActionPreference
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
$nssmOutput = & nssm status $Config.ServiceName 2>&1
|
||||
$serviceExists = $LASTEXITCODE -eq 0
|
||||
|
||||
$ErrorActionPreference = $oldErrorAction
|
||||
|
||||
if ($serviceExists) {
|
||||
Write-Warning "Service already exists, stopping and removing..."
|
||||
& nssm stop $Config.ServiceName 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
& nssm remove $Config.ServiceName confirm 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Success "Existing service removed"
|
||||
}
|
||||
|
||||
# Get Python path from virtual environment
|
||||
$venvPath = Join-Path $Config.InstallPath "venv"
|
||||
$pythonPath = Join-Path $venvPath "Scripts\python.exe"
|
||||
|
||||
if (-not (Test-Path $pythonPath)) {
|
||||
throw "Virtual environment not found. Please run Install-PythonDependencies first."
|
||||
}
|
||||
|
||||
$appModule = "-m"
|
||||
$appMain = "app.main"
|
||||
|
||||
# NSSM service creation
|
||||
try {
|
||||
# Install service
|
||||
& nssm install $Config.ServiceName $pythonPath $appModule $appMain
|
||||
|
||||
# Set service configuration
|
||||
& nssm set $Config.ServiceName DisplayName $Config.ServiceDisplayName
|
||||
& nssm set $Config.ServiceName Description $Config.ServiceDescription
|
||||
& nssm set $Config.ServiceName Start SERVICE_AUTO_START
|
||||
& nssm set $Config.ServiceName AppDirectory $Config.InstallPath
|
||||
|
||||
# Set environment variables
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
if (Test-Path $envFile) {
|
||||
& nssm set $Config.ServiceName AppEnvironmentExtra "PYTHONPATH=$($Config.InstallPath)"
|
||||
Write-Success ".env file will be loaded by application"
|
||||
} else {
|
||||
Write-Warning ".env file not found - create it before starting service"
|
||||
}
|
||||
|
||||
# Set logging
|
||||
$stdoutLog = Join-Path $Config.LogsPath "stdout.log"
|
||||
$stderrLog = Join-Path $Config.LogsPath "stderr.log"
|
||||
& nssm set $Config.ServiceName AppStdout $stdoutLog
|
||||
& nssm set $Config.ServiceName AppStderr $stderrLog
|
||||
& nssm set $Config.ServiceName AppStdoutCreationDisposition 4
|
||||
& nssm set $Config.ServiceName AppStderrCreationDisposition 4
|
||||
|
||||
# Set restart policy (important for bot reliability)
|
||||
& nssm set $Config.ServiceName AppExit Default Restart
|
||||
& nssm set $Config.ServiceName AppRestartDelay 5000
|
||||
& nssm set $Config.ServiceName AppThrottle 10000
|
||||
|
||||
Write-Success "Windows Service created successfully"
|
||||
} catch {
|
||||
throw "Failed to create Windows Service: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-ConfigurationFile {
|
||||
Write-Step "Creating configuration template..."
|
||||
|
||||
$envExample = Join-Path $Config.InstallPath ".env.example"
|
||||
$envFile = Join-Path $Config.InstallPath ".env"
|
||||
|
||||
# Create .env.example template
|
||||
$envTemplate = @"
|
||||
# ROA2WEB Telegram Bot - Production Configuration
|
||||
|
||||
# Telegram Bot Configuration
|
||||
TELEGRAM_BOT_TOKEN=your_production_bot_token_here
|
||||
|
||||
# Claude Authentication Configuration
|
||||
# =====================================
|
||||
# Two authentication methods are supported:
|
||||
#
|
||||
# Method 1: Claude Pro/Max Subscription (RECOMMENDED)
|
||||
# - Leave CLAUDE_API_KEY empty or remove the line
|
||||
# - Run: scripts\Setup-ClaudeAuth.ps1
|
||||
# - Authenticate via browser with your Claude Pro/Max account
|
||||
# - No additional costs!
|
||||
#
|
||||
# Method 2: Claude API Key (Alternative)
|
||||
# - Get API key from: https://console.anthropic.com/settings/keys
|
||||
# - Set CLAUDE_API_KEY below
|
||||
# - This will take precedence over browser login
|
||||
# - Usage-based billing applies
|
||||
#
|
||||
# Leave empty to use Claude Pro/Max subscription:
|
||||
CLAUDE_API_KEY=
|
||||
|
||||
# Backend API Configuration
|
||||
BACKEND_URL=http://localhost:8000
|
||||
BACKEND_TIMEOUT=30
|
||||
|
||||
# SQLite Database Configuration
|
||||
SQLITE_DB_PATH=$($Config.DataPath -replace '\\', '\\')\telegram_bot.db
|
||||
|
||||
# Internal API Configuration (for backend callbacks)
|
||||
INTERNAL_API_HOST=127.0.0.1
|
||||
INTERNAL_API_PORT=$($Config.ServicePort)
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FILE=$($Config.LogsPath -replace '\\', '\\')\bot.log
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT=production
|
||||
|
||||
# Authentication Configuration
|
||||
AUTH_CODE_EXPIRY_MINUTES=15
|
||||
JWT_REFRESH_THRESHOLD_MINUTES=5
|
||||
|
||||
# Session Configuration
|
||||
SESSION_TIMEOUT_MINUTES=60
|
||||
MAX_CONVERSATION_HISTORY=20
|
||||
"@
|
||||
|
||||
# Write .env.example
|
||||
Set-Content -Path $envExample -Value $envTemplate -Encoding UTF8
|
||||
Write-Success "Created .env.example template"
|
||||
|
||||
# Copy to .env if it doesn't exist
|
||||
if (-not (Test-Path $envFile)) {
|
||||
Copy-Item -Path $envExample -Destination $envFile
|
||||
Write-Warning "Created .env file - PLEASE UPDATE WITH PRODUCTION VALUES"
|
||||
Write-Host " Edit: $envFile" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Success ".env file already exists (not overwriting)"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ServiceHealth {
|
||||
Write-Step "Testing service health..."
|
||||
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:$($Config.ServicePort)/internal/health" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
$content = $response.Content | ConvertFrom-Json
|
||||
Write-Success "Health check passed: $($content.status)"
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Health check failed (service may need configuration): $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Summary {
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " ROA2WEB TELEGRAM BOT INSTALLATION COMPLETED" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nInstallation Details:" -ForegroundColor Yellow
|
||||
Write-Host " Source Path: $($Config.SourcePath)"
|
||||
Write-Host " Install Path: $($Config.InstallPath)"
|
||||
Write-Host " Scripts Path: $($Config.InstallPath)\scripts\"
|
||||
Write-Host " Data Path: $($Config.DataPath)"
|
||||
Write-Host " Logs Path: $($Config.LogsPath)"
|
||||
Write-Host " Backup Path: $($Config.BackupPath)"
|
||||
Write-Host " Service Name: $($Config.ServiceName)"
|
||||
Write-Host " Internal API Port: $($Config.ServicePort)"
|
||||
|
||||
Write-Host "`nService Endpoints:" -ForegroundColor Yellow
|
||||
Write-Host " Health Check: http://localhost:$($Config.ServicePort)/internal/health"
|
||||
Write-Host " Stats: http://localhost:$($Config.ServicePort)/internal/stats"
|
||||
|
||||
Write-Host "`nNext Steps:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Edit configuration: $($Config.InstallPath)\.env"
|
||||
Write-Host " - Set TELEGRAM_BOT_TOKEN (from @BotFather)"
|
||||
Write-Host " - Set CLAUDE_API_KEY (from Anthropic console)"
|
||||
Write-Host " - Verify BACKEND_URL=http://localhost:8000"
|
||||
Write-Host " 2. Navigate to scripts: cd $($Config.InstallPath)\scripts"
|
||||
Write-Host " 3. Start service: .\Start-TelegramBot.ps1"
|
||||
Write-Host " 4. Check logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50"
|
||||
|
||||
Write-Host "`nManagement Scripts Location:" -ForegroundColor Yellow
|
||||
Write-Host " $($Config.InstallPath)\scripts\"
|
||||
|
||||
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
|
||||
Write-Host " cd $($Config.InstallPath)\scripts"
|
||||
Write-Host " .\Start-TelegramBot.ps1 # Start service"
|
||||
Write-Host " .\Stop-TelegramBot.ps1 # Stop service"
|
||||
Write-Host " .\Restart-TelegramBot.ps1 # Restart service"
|
||||
Write-Host " .\Backup-TelegramDB.ps1 # Backup database"
|
||||
Write-Host " .\Setup-DailyBackup.ps1 # Setup automated daily backups"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN INSTALLATION FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Windows Server Installation Script
|
||||
Telegram Bot Frontend with Claude Agent SDK
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
try {
|
||||
# Installation steps
|
||||
Test-PythonInstallation
|
||||
Install-NSSM
|
||||
New-DirectoryStructure
|
||||
|
||||
# Copy application files from deployment package
|
||||
$filesCopied = Copy-ApplicationFiles
|
||||
if (-not $filesCopied) {
|
||||
throw "Failed to copy application files. Please ensure you're running this script from the deployment package directory."
|
||||
}
|
||||
|
||||
Install-PythonDependencies
|
||||
New-ConfigurationFile
|
||||
New-WindowsService
|
||||
Show-Summary
|
||||
|
||||
Write-Host "`nInstallation completed successfully!" -ForegroundColor Green
|
||||
Write-Host "IMPORTANT: Configure .env file before starting service" -ForegroundColor Yellow
|
||||
|
||||
} catch {
|
||||
Write-Host "`n[FATAL ERROR] Installation failed: $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main installation
|
||||
Main
|
||||
38
deployment/windows/scripts/Restart-ROA2WEB.ps1
Normal file
38
deployment/windows/scripts/Restart-ROA2WEB.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restart ROA2WEB Backend Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops and starts the ROA2WEB backend Windows service.
|
||||
|
||||
.EXAMPLE
|
||||
.\Restart-ROA2WEB.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-Backend"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Restarting ROA2WEB Backend Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Stop service
|
||||
Write-Host "`n[*] Stopping service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Stop-ROA2WEB.ps1" -ServiceName $ServiceName
|
||||
|
||||
# Wait a moment
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Start service
|
||||
Write-Host "`n[*] Starting service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Start-ROA2WEB.ps1" -ServiceName $ServiceName
|
||||
|
||||
Write-Host "`n[OK] Service restarted successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "`n[ERROR] Failed to restart service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
38
deployment/windows/scripts/Restart-TelegramBot.ps1
Normal file
38
deployment/windows/scripts/Restart-TelegramBot.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Restart ROA2WEB Telegram Bot Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops and starts the ROA2WEB Telegram Bot Windows service.
|
||||
|
||||
.EXAMPLE
|
||||
.\Restart-TelegramBot.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-TelegramBot"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Restarting ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# Stop service
|
||||
Write-Host "`n[*] Stopping service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Stop-TelegramBot.ps1" -ServiceName $ServiceName
|
||||
|
||||
# Wait a moment
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
# Start service
|
||||
Write-Host "`n[*] Starting service..." -ForegroundColor Yellow
|
||||
& "$PSScriptRoot\Start-TelegramBot.ps1" -ServiceName $ServiceName
|
||||
|
||||
Write-Host "`n[OK] Service restarted successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "`n[ERROR] Failed to restart service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
365
deployment/windows/scripts/Setup-DailyBackup.ps1
Normal file
365
deployment/windows/scripts/Setup-DailyBackup.ps1
Normal file
@@ -0,0 +1,365 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Setup Daily Backup Task for ROA2WEB Telegram Bot Database
|
||||
|
||||
.DESCRIPTION
|
||||
This script configures Windows Task Scheduler to run daily database backups:
|
||||
- Creates scheduled task (ROA2WEB-TelegramBot-Backup)
|
||||
- Runs daily at specified time (default: 2:00 AM)
|
||||
- Executes Backup-TelegramDB.ps1 script
|
||||
- Runs as SYSTEM account
|
||||
- Logs all backup operations
|
||||
- Optional email notifications on failure
|
||||
|
||||
.PARAMETER BackupTime
|
||||
Daily backup time in 24-hour format (default: "02:00")
|
||||
|
||||
.PARAMETER TaskName
|
||||
Name of the scheduled task (default: ROA2WEB-TelegramBot-Backup)
|
||||
|
||||
.PARAMETER InstallPath
|
||||
Installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot)
|
||||
|
||||
.PARAMETER RunAsUser
|
||||
User account to run task (default: SYSTEM)
|
||||
|
||||
.PARAMETER EnableEmailAlerts
|
||||
Enable email notifications on backup failure (default: false)
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-DailyBackup.ps1
|
||||
Setup daily backup at 2:00 AM
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-DailyBackup.ps1 -BackupTime "03:30"
|
||||
Setup daily backup at 3:30 AM
|
||||
|
||||
.EXAMPLE
|
||||
.\Setup-DailyBackup.ps1 -EnableEmailAlerts $true
|
||||
Setup with email notifications on failure
|
||||
|
||||
.NOTES
|
||||
Author: ROA2WEB Team
|
||||
Requires: PowerShell 5.1+, Administrator privileges
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BackupTime = "02:00",
|
||||
[string]$TaskName = "ROA2WEB-TelegramBot-Backup",
|
||||
[string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot",
|
||||
[string]$RunAsUser = "SYSTEM",
|
||||
[bool]$EnableEmailAlerts = $false
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n[*] $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-Host " [OK] $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Error {
|
||||
param([string]$Message)
|
||||
Write-Host " [ERROR] $Message" -ForegroundColor Red
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-Host " [WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]$identity
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
function Test-BackupScript {
|
||||
$backupScriptPath = Join-Path $PSScriptRoot "Backup-TelegramDB.ps1"
|
||||
|
||||
if (-not (Test-Path $backupScriptPath)) {
|
||||
throw "Backup script not found: $backupScriptPath"
|
||||
}
|
||||
|
||||
Write-Success "Backup script found: $backupScriptPath"
|
||||
return $backupScriptPath
|
||||
}
|
||||
|
||||
function Remove-ExistingTask {
|
||||
param([string]$TaskName)
|
||||
|
||||
Write-Step "Checking for existing scheduled task..."
|
||||
|
||||
try {
|
||||
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingTask) {
|
||||
Write-Warning "Existing task found, removing..."
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
Write-Success "Existing task removed"
|
||||
} else {
|
||||
Write-Success "No existing task found"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not check for existing task: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function New-ScheduledBackupTask {
|
||||
param(
|
||||
[string]$TaskName,
|
||||
[string]$BackupScriptPath,
|
||||
[string]$BackupTime,
|
||||
[string]$RunAsUser
|
||||
)
|
||||
|
||||
Write-Step "Creating scheduled task..."
|
||||
|
||||
try {
|
||||
# Parse backup time
|
||||
$timeComponents = $BackupTime -split ":"
|
||||
$hour = [int]$timeComponents[0]
|
||||
$minute = [int]$timeComponents[1]
|
||||
|
||||
# Create task action (run PowerShell script)
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "PowerShell.exe" `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$BackupScriptPath`""
|
||||
|
||||
# Create task trigger (daily at specified time)
|
||||
$trigger = New-ScheduledTaskTrigger `
|
||||
-Daily `
|
||||
-At (Get-Date).Date.AddHours($hour).AddMinutes($minute)
|
||||
|
||||
# Create task settings
|
||||
$settings = New-ScheduledTaskSettings `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-RunOnlyIfNetworkAvailable:$false `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
|
||||
# Create task principal (run as specified user)
|
||||
if ($RunAsUser -eq "SYSTEM") {
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId "SYSTEM" `
|
||||
-LogonType ServiceAccount `
|
||||
-RunLevel Highest
|
||||
} else {
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId $RunAsUser `
|
||||
-LogonType Password `
|
||||
-RunLevel Highest
|
||||
}
|
||||
|
||||
# Register task
|
||||
$task = Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Settings $settings `
|
||||
-Principal $principal `
|
||||
-Description "Daily backup of ROA2WEB Telegram Bot SQLite database"
|
||||
|
||||
Write-Success "Scheduled task created: $TaskName"
|
||||
Write-Success "Schedule: Daily at $BackupTime"
|
||||
Write-Success "Run as: $RunAsUser"
|
||||
|
||||
return $task
|
||||
} catch {
|
||||
throw "Failed to create scheduled task: $_"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TaskCreation {
|
||||
param([string]$TaskName)
|
||||
|
||||
Write-Step "Verifying task creation..."
|
||||
|
||||
try {
|
||||
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
|
||||
Write-Success "Task verified: $TaskName"
|
||||
Write-Host " Task State: $($task.State)" -ForegroundColor Gray
|
||||
Write-Host " Last Run: $($taskInfo.LastRunTime)" -ForegroundColor Gray
|
||||
Write-Host " Last Result: $($taskInfo.LastTaskResult)" -ForegroundColor Gray
|
||||
Write-Host " Next Run: $($taskInfo.NextRunTime)" -ForegroundColor Gray
|
||||
|
||||
return $true
|
||||
} catch {
|
||||
Write-Error "Task verification failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TaskExecution {
|
||||
param([string]$TaskName)
|
||||
|
||||
Write-Step "Testing task execution..."
|
||||
|
||||
try {
|
||||
# Run task immediately
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
|
||||
Write-Success "Task execution started"
|
||||
Write-Host " Waiting for task to complete..." -ForegroundColor Yellow
|
||||
|
||||
# Wait for task to complete (max 60 seconds)
|
||||
$timeout = 60
|
||||
$elapsed = 0
|
||||
|
||||
while ($elapsed -lt $timeout) {
|
||||
Start-Sleep -Seconds 2
|
||||
$elapsed += 2
|
||||
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
$task = Get-ScheduledTask -TaskName $TaskName
|
||||
|
||||
if ($task.State -ne "Running") {
|
||||
break
|
||||
}
|
||||
|
||||
Write-Host " Task still running... ($elapsed/$timeout seconds)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Check result
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
|
||||
if ($taskInfo.LastTaskResult -eq 0) {
|
||||
Write-Success "Task executed successfully"
|
||||
Write-Success "Last run: $($taskInfo.LastRunTime)"
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "Task completed with result code: $($taskInfo.LastTaskResult)"
|
||||
Write-Host " Check backup logs for details" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
Write-Error "Task execution test failed: $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-TaskSummary {
|
||||
param([string]$TaskName, [string]$BackupTime, [string]$InstallPath)
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
Write-Host " DAILY BACKUP TASK CONFIGURED SUCCESSFULLY" -ForegroundColor Green
|
||||
Write-Host ("=" * 80) -ForegroundColor Cyan
|
||||
|
||||
Write-Host "`nTask Configuration:" -ForegroundColor Yellow
|
||||
Write-Host " Task Name: $TaskName"
|
||||
Write-Host " Schedule: Daily at $BackupTime"
|
||||
Write-Host " Run As: $RunAsUser"
|
||||
Write-Host " Installation Path: $InstallPath"
|
||||
|
||||
$task = Get-ScheduledTask -TaskName $TaskName
|
||||
$taskInfo = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
|
||||
Write-Host "`nTask Status:" -ForegroundColor Yellow
|
||||
Write-Host " State: $($task.State)"
|
||||
Write-Host " Last Run: $($taskInfo.LastRunTime)"
|
||||
Write-Host " Last Result: $($taskInfo.LastTaskResult)"
|
||||
Write-Host " Next Run: $($taskInfo.NextRunTime)"
|
||||
|
||||
Write-Host "`nManagement Commands:" -ForegroundColor Yellow
|
||||
Write-Host " View task: Get-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Run manually: Start-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Disable task: Disable-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Enable task: Enable-ScheduledTask -TaskName '$TaskName'"
|
||||
Write-Host " Remove task: Unregister-ScheduledTask -TaskName '$TaskName'"
|
||||
|
||||
Write-Host "`nBackup Management:" -ForegroundColor Yellow
|
||||
Write-Host " Manual backup: .\Backup-TelegramDB.ps1"
|
||||
Write-Host " Backup logs: Get-Content $InstallPath\logs\backup.log -Tail 50"
|
||||
Write-Host " Backups location: $InstallPath\backups"
|
||||
|
||||
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# MAIN SETUP FLOW
|
||||
# =============================================================================
|
||||
|
||||
function Main {
|
||||
Write-Host @"
|
||||
|
||||
====================================================================
|
||||
ROA2WEB Telegram Bot - Setup Daily Backup Task
|
||||
Configure automated database backup via Task Scheduler
|
||||
====================================================================
|
||||
|
||||
"@ -ForegroundColor Cyan
|
||||
|
||||
# Check prerequisites
|
||||
Write-Step "Checking prerequisites..."
|
||||
|
||||
if (-not (Test-Administrator)) {
|
||||
Write-Error "This script must be run as Administrator"
|
||||
Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Running as Administrator"
|
||||
|
||||
# Find backup script
|
||||
$backupScriptPath = Test-BackupScript
|
||||
|
||||
# Validate installation path
|
||||
if (-not (Test-Path $InstallPath)) {
|
||||
Write-Error "Installation path not found: $InstallPath"
|
||||
Write-Host " Run Install-TelegramBot.ps1 first" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Success "Installation path verified"
|
||||
|
||||
try {
|
||||
# Setup task
|
||||
Remove-ExistingTask -TaskName $TaskName
|
||||
$task = New-ScheduledBackupTask `
|
||||
-TaskName $TaskName `
|
||||
-BackupScriptPath $backupScriptPath `
|
||||
-BackupTime $BackupTime `
|
||||
-RunAsUser $RunAsUser
|
||||
|
||||
# Verify task creation
|
||||
$verified = Test-TaskCreation -TaskName $TaskName
|
||||
|
||||
if ($verified) {
|
||||
# Test task execution
|
||||
Write-Host "`nDo you want to test the backup task now? (Y/N)" -ForegroundColor Yellow
|
||||
$response = Read-Host
|
||||
|
||||
if ($response -eq "Y" -or $response -eq "y") {
|
||||
Test-TaskExecution -TaskName $TaskName
|
||||
} else {
|
||||
Write-Host " Skipping test execution" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Show summary
|
||||
Show-TaskSummary -TaskName $TaskName -BackupTime $BackupTime -InstallPath $InstallPath
|
||||
|
||||
Write-Host "`nSetup completed successfully!" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
throw "Task verification failed"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "`n[SETUP FAILED] $_" -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Run main setup
|
||||
Main
|
||||
66
deployment/windows/scripts/Start-ROA2WEB.ps1
Normal file
66
deployment/windows/scripts/Start-ROA2WEB.ps1
Normal file
@@ -0,0 +1,66 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start ROA2WEB Backend Service
|
||||
|
||||
.DESCRIPTION
|
||||
Starts the ROA2WEB backend Windows service and validates it's running properly.
|
||||
|
||||
.EXAMPLE
|
||||
.\Start-ROA2WEB.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-Backend",
|
||||
[int]$Timeout = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Starting ROA2WEB Backend Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service is already running" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Start service
|
||||
Start-Service -Name $ServiceName
|
||||
Write-Host " [*] Service start command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to start
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to start... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service started successfully" -ForegroundColor Green
|
||||
|
||||
# Wait a bit and test health endpoint
|
||||
Start-Sleep -Seconds 3
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:8000/health" -UseBasicParsing -TimeoutSec 5
|
||||
if ($response.StatusCode -eq 200) {
|
||||
Write-Host " [OK] Health check passed" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [WARN] Health check failed (service may still be starting)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service failed to start (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\logs\backend-stderr.log" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to start service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
70
deployment/windows/scripts/Start-TelegramBot.ps1
Normal file
70
deployment/windows/scripts/Start-TelegramBot.ps1
Normal file
@@ -0,0 +1,70 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start ROA2WEB Telegram Bot Service
|
||||
|
||||
.DESCRIPTION
|
||||
Starts the ROA2WEB Telegram Bot Windows service and validates it's running properly.
|
||||
|
||||
.EXAMPLE
|
||||
.\Start-TelegramBot.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-TelegramBot",
|
||||
[int]$Timeout = 30,
|
||||
[int]$HealthPort = 8002
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Starting ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service is already running" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Start service
|
||||
Start-Service -Name $ServiceName
|
||||
Write-Host " [*] Service start command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to start
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Running" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to start... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Running") {
|
||||
Write-Host " [OK] Service started successfully" -ForegroundColor Green
|
||||
|
||||
# Wait a bit and test health endpoint
|
||||
Start-Sleep -Seconds 5
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://localhost:$HealthPort/internal/health" -UseBasicParsing -TimeoutSec 10
|
||||
if ($response.StatusCode -eq 200) {
|
||||
$content = $response.Content | ConvertFrom-Json
|
||||
Write-Host " [OK] Health check passed: $($content.status)" -ForegroundColor Green
|
||||
Write-Host " [OK] Database: $($content.database.status)" -ForegroundColor Green
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [WARN] Health check failed (service may still be starting): $_" -ForegroundColor Yellow
|
||||
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service failed to start (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " Check logs: C:\inetpub\wwwroot\roa2web\telegram-bot\logs\stderr.log" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to start service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
54
deployment/windows/scripts/Stop-ROA2WEB.ps1
Normal file
54
deployment/windows/scripts/Stop-ROA2WEB.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stop ROA2WEB Backend Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops the ROA2WEB backend Windows service gracefully.
|
||||
|
||||
.EXAMPLE
|
||||
.\Stop-ROA2WEB.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-Backend",
|
||||
[int]$Timeout = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Stopping ROA2WEB Backend Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service is already stopped" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Stop service
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
Write-Host " [*] Service stop command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to stop
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to stop... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service stopped successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service did not stop within timeout (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " You may need to force kill the process" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to stop service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
54
deployment/windows/scripts/Stop-TelegramBot.ps1
Normal file
54
deployment/windows/scripts/Stop-TelegramBot.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stop ROA2WEB Telegram Bot Service
|
||||
|
||||
.DESCRIPTION
|
||||
Stops the ROA2WEB Telegram Bot Windows service gracefully.
|
||||
|
||||
.EXAMPLE
|
||||
.\Stop-TelegramBot.ps1
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ServiceName = "ROA2WEB-TelegramBot",
|
||||
[int]$Timeout = 30
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "`n[*] Stopping ROA2WEB Telegram Bot Service..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
$service = Get-Service -Name $ServiceName -ErrorAction Stop
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service is already stopped" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Stop service
|
||||
Stop-Service -Name $ServiceName -Force
|
||||
Write-Host " [*] Service stop command issued" -ForegroundColor Yellow
|
||||
|
||||
# Wait for service to stop
|
||||
$elapsed = 0
|
||||
while ($service.Status -ne "Stopped" -and $elapsed -lt $Timeout) {
|
||||
Start-Sleep -Seconds 1
|
||||
$service.Refresh()
|
||||
$elapsed++
|
||||
Write-Host " [*] Waiting for service to stop... ($elapsed/$Timeout)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if ($service.Status -eq "Stopped") {
|
||||
Write-Host " [OK] Service stopped successfully" -ForegroundColor Green
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host " [ERROR] Service did not stop within timeout (Status: $($service.Status))" -ForegroundColor Red
|
||||
Write-Host " You may need to force kill the process" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [ERROR] Failed to stop service: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user