feat: Add AlexHost deployment pipeline and bot control functionality
- Add Gitea Actions workflow for manual AlexHost deployment - Add docker-compose.alexhost.yml for production deployment - Add deploy-alexhost.sh script with server-side build support - Add Bot Control feature (Start/Stop/Restart) for remote bot management - Add discovery control endpoint in TeleBot - Update TeleBot with StartPollingAsync/StopPolling/RestartPollingAsync - Fix platform architecture issues by building on target server - Update docker-compose configurations for all environments Deployment tested successfully: - TeleShop: healthy at https://teleshop.silentmary.mywire.org - TeleBot: healthy with discovery integration - SilverPay: connectivity verified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0997cc8c57
commit
86f19ba044
193
.gitea/workflows/deploy-alexhost.yml
Normal file
193
.gitea/workflows/deploy-alexhost.yml
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# Gitea Actions Workflow for AlexHost Deployment
|
||||||
|
# This workflow provides manual deployment to the AlexHost production server
|
||||||
|
# Server: 193.233.245.41 (teleshop.silentmary.mywire.org)
|
||||||
|
|
||||||
|
name: Deploy to AlexHost
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
deploy_teleshop:
|
||||||
|
description: 'Deploy TeleShop (LittleShop)'
|
||||||
|
required: true
|
||||||
|
default: 'true'
|
||||||
|
type: boolean
|
||||||
|
deploy_telebot:
|
||||||
|
description: 'Deploy TeleBot'
|
||||||
|
required: true
|
||||||
|
default: 'true'
|
||||||
|
type: boolean
|
||||||
|
force_rebuild:
|
||||||
|
description: 'Force rebuild without cache'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
ALEXHOST_IP: 193.233.245.41
|
||||||
|
ALEXHOST_USER: sysadmin
|
||||||
|
REGISTRY: localhost:5000
|
||||||
|
TELESHOP_IMAGE: littleshop
|
||||||
|
TELEBOT_IMAGE: telebot
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build TeleShop Image
|
||||||
|
if: ${{ inputs.deploy_teleshop == 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "Building TeleShop image..."
|
||||||
|
CACHE_FLAG=""
|
||||||
|
if [ "${{ inputs.force_rebuild }}" = "true" ]; then
|
||||||
|
CACHE_FLAG="--no-cache"
|
||||||
|
fi
|
||||||
|
docker build $CACHE_FLAG -t ${{ env.TELESHOP_IMAGE }}:${{ github.sha }} -t ${{ env.TELESHOP_IMAGE }}:latest -f Dockerfile .
|
||||||
|
docker save ${{ env.TELESHOP_IMAGE }}:latest | gzip > teleshop-image.tar.gz
|
||||||
|
echo "TeleShop image built successfully"
|
||||||
|
|
||||||
|
- name: Build TeleBot Image
|
||||||
|
if: ${{ inputs.deploy_telebot == 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "Building TeleBot image..."
|
||||||
|
CACHE_FLAG=""
|
||||||
|
if [ "${{ inputs.force_rebuild }}" = "true" ]; then
|
||||||
|
CACHE_FLAG="--no-cache"
|
||||||
|
fi
|
||||||
|
docker build $CACHE_FLAG -t ${{ env.TELEBOT_IMAGE }}:${{ github.sha }} -t ${{ env.TELEBOT_IMAGE }}:latest -f Dockerfile.telebot .
|
||||||
|
docker save ${{ env.TELEBOT_IMAGE }}:latest | gzip > telebot-image.tar.gz
|
||||||
|
echo "TeleBot image built successfully"
|
||||||
|
|
||||||
|
- name: Configure SSH
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.ALEXHOST_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan -H ${{ env.ALEXHOST_IP }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
|
||||||
|
- name: Copy TeleShop Image to AlexHost
|
||||||
|
if: ${{ inputs.deploy_teleshop == 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "Transferring TeleShop image to AlexHost..."
|
||||||
|
scp -o StrictHostKeyChecking=no teleshop-image.tar.gz ${{ env.ALEXHOST_USER }}@${{ env.ALEXHOST_IP }}:/tmp/
|
||||||
|
echo "TeleShop image transferred"
|
||||||
|
|
||||||
|
- name: Copy TeleBot Image to AlexHost
|
||||||
|
if: ${{ inputs.deploy_telebot == 'true' }}
|
||||||
|
run: |
|
||||||
|
echo "Transferring TeleBot image to AlexHost..."
|
||||||
|
scp -o StrictHostKeyChecking=no telebot-image.tar.gz ${{ env.ALEXHOST_USER }}@${{ env.ALEXHOST_IP }}:/tmp/
|
||||||
|
echo "TeleBot image transferred"
|
||||||
|
|
||||||
|
- name: Copy Docker Compose to AlexHost
|
||||||
|
run: |
|
||||||
|
echo "Copying deployment files..."
|
||||||
|
scp -o StrictHostKeyChecking=no docker-compose.alexhost.yml ${{ env.ALEXHOST_USER }}@${{ env.ALEXHOST_IP }}:/tmp/
|
||||||
|
|
||||||
|
- name: Deploy TeleShop on AlexHost
|
||||||
|
if: ${{ inputs.deploy_teleshop == 'true' }}
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no ${{ env.ALEXHOST_USER }}@${{ env.ALEXHOST_IP }} << 'DEPLOY_EOF'
|
||||||
|
set -e
|
||||||
|
echo "=== Deploying TeleShop ==="
|
||||||
|
|
||||||
|
# Load image
|
||||||
|
echo "Loading TeleShop image..."
|
||||||
|
gunzip -c /tmp/teleshop-image.tar.gz | sudo docker load
|
||||||
|
|
||||||
|
# Tag and push to local registry
|
||||||
|
echo "Pushing to local registry..."
|
||||||
|
sudo docker tag littleshop:latest localhost:5000/littleshop:latest
|
||||||
|
sudo docker push localhost:5000/littleshop:latest
|
||||||
|
|
||||||
|
# Stop and remove existing container
|
||||||
|
echo "Stopping existing container..."
|
||||||
|
sudo docker stop teleshop 2>/dev/null || true
|
||||||
|
sudo docker rm teleshop 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start new container using compose
|
||||||
|
echo "Starting new container..."
|
||||||
|
cd /home/sysadmin/teleshop-source 2>/dev/null || mkdir -p /home/sysadmin/teleshop-source
|
||||||
|
cp /tmp/docker-compose.alexhost.yml /home/sysadmin/teleshop-source/docker-compose.yml
|
||||||
|
cd /home/sysadmin/teleshop-source
|
||||||
|
sudo docker compose up -d teleshop
|
||||||
|
|
||||||
|
# Wait for health check
|
||||||
|
echo "Waiting for health check..."
|
||||||
|
sleep 30
|
||||||
|
if sudo docker ps | grep -q "teleshop.*healthy"; then
|
||||||
|
echo "TeleShop deployed successfully!"
|
||||||
|
else
|
||||||
|
echo "Warning: Container may still be starting..."
|
||||||
|
sudo docker ps | grep teleshop
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm /tmp/teleshop-image.tar.gz
|
||||||
|
echo "=== TeleShop deployment complete ==="
|
||||||
|
DEPLOY_EOF
|
||||||
|
|
||||||
|
- name: Deploy TeleBot on AlexHost
|
||||||
|
if: ${{ inputs.deploy_telebot == 'true' }}
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no ${{ env.ALEXHOST_USER }}@${{ env.ALEXHOST_IP }} << 'DEPLOY_EOF'
|
||||||
|
set -e
|
||||||
|
echo "=== Deploying TeleBot ==="
|
||||||
|
|
||||||
|
# Load image
|
||||||
|
echo "Loading TeleBot image..."
|
||||||
|
gunzip -c /tmp/telebot-image.tar.gz | sudo docker load
|
||||||
|
|
||||||
|
# Tag and push to local registry
|
||||||
|
echo "Pushing to local registry..."
|
||||||
|
sudo docker tag telebot:latest localhost:5000/telebot:latest
|
||||||
|
sudo docker push localhost:5000/telebot:latest
|
||||||
|
|
||||||
|
# Stop and remove existing container
|
||||||
|
echo "Stopping existing container..."
|
||||||
|
sudo docker stop telebot 2>/dev/null || true
|
||||||
|
sudo docker rm telebot 2>/dev/null || true
|
||||||
|
|
||||||
|
# Start new container using compose
|
||||||
|
echo "Starting new container..."
|
||||||
|
cd /home/sysadmin/teleshop-source
|
||||||
|
sudo docker compose up -d telebot
|
||||||
|
|
||||||
|
# Wait for startup
|
||||||
|
echo "Waiting for startup..."
|
||||||
|
sleep 20
|
||||||
|
sudo docker ps | grep telebot
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm /tmp/telebot-image.tar.gz
|
||||||
|
echo "=== TeleBot deployment complete ==="
|
||||||
|
DEPLOY_EOF
|
||||||
|
|
||||||
|
- name: Verify Deployment
|
||||||
|
run: |
|
||||||
|
ssh -o StrictHostKeyChecking=no ${{ env.ALEXHOST_USER }}@${{ env.ALEXHOST_IP }} << 'VERIFY_EOF'
|
||||||
|
echo "=== Deployment Verification ==="
|
||||||
|
echo ""
|
||||||
|
echo "Running Containers:"
|
||||||
|
sudo docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||||
|
echo ""
|
||||||
|
echo "Testing TeleShop health..."
|
||||||
|
curl -sf http://localhost:5100/health && echo "TeleShop: OK" || echo "TeleShop: FAIL"
|
||||||
|
echo ""
|
||||||
|
echo "Testing TeleBot health..."
|
||||||
|
curl -sf http://localhost:5010/health 2>/dev/null && echo "TeleBot: OK" || echo "TeleBot: API endpoint not exposed (normal for bot-only mode)"
|
||||||
|
echo ""
|
||||||
|
echo "=== Verification complete ==="
|
||||||
|
VERIFY_EOF
|
||||||
|
|
||||||
|
- name: Cleanup Local Artifacts
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f teleshop-image.tar.gz telebot-image.tar.gz
|
||||||
|
echo "Cleanup complete"
|
||||||
@ -74,7 +74,7 @@ ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=0 \
|
|||||||
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
|
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
|
||||||
ASPNETCORE_URLS=http://+:8080 \
|
ASPNETCORE_URLS=http://+:8080 \
|
||||||
ASPNETCORE_ENVIRONMENT=Production \
|
ASPNETCORE_ENVIRONMENT=Production \
|
||||||
ConnectionStrings__DefaultConnection="Data Source=/app/data/teleshop-prod.db;Cache=Shared" \
|
ConnectionStrings__DefaultConnection="Data Source=/app/data/littleshop-production.db;Cache=Shared" \
|
||||||
SilverPay__BaseUrl="http://31.97.57.205:8001" \
|
SilverPay__BaseUrl="http://31.97.57.205:8001" \
|
||||||
SilverPay__ApiKey="your-api-key-here" \
|
SilverPay__ApiKey="your-api-key-here" \
|
||||||
TMPDIR=/tmp
|
TMPDIR=/tmp
|
||||||
|
|||||||
@ -793,5 +793,131 @@ public class BotsController : Controller
|
|||||||
await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status);
|
await _botService.UpdateRemoteInfoAsync(botId, ipAddress, port, instanceId, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/StartBot/5
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> StartBot(Guid id)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Start bot requested for {BotId}", id);
|
||||||
|
|
||||||
|
var bot = await _botService.GetBotByIdAsync(id);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot not found";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bot.IsRemote)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot control is only available for remote bots";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _discoveryService.ControlBotAsync(id, "start");
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = result.Message;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = result.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to start bot {BotId}", id);
|
||||||
|
TempData["Error"] = $"Failed to start bot: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/StopBot/5
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> StopBot(Guid id)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Stop bot requested for {BotId}", id);
|
||||||
|
|
||||||
|
var bot = await _botService.GetBotByIdAsync(id);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot not found";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bot.IsRemote)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot control is only available for remote bots";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _discoveryService.ControlBotAsync(id, "stop");
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = result.Message;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = result.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to stop bot {BotId}", id);
|
||||||
|
TempData["Error"] = $"Failed to stop bot: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Admin/Bots/RestartBot/5
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RestartBot(Guid id)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Restart bot requested for {BotId}", id);
|
||||||
|
|
||||||
|
var bot = await _botService.GetBotByIdAsync(id);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot not found";
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bot.IsRemote)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Bot control is only available for remote bots";
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _discoveryService.ControlBotAsync(id, "restart");
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
|
{
|
||||||
|
TempData["Success"] = result.Message;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = result.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to restart bot {BotId}", id);
|
||||||
|
TempData["Error"] = $"Failed to restart bot: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@ -61,6 +61,11 @@ public class ProductsController : Controller
|
|||||||
|
|
||||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||||
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
||||||
|
|
||||||
|
// FIX: Re-populate VariantCollections for view rendering when validation fails
|
||||||
|
var variantCollections = await _variantCollectionService.GetAllVariantCollectionsAsync();
|
||||||
|
ViewData["VariantCollections"] = variantCollections.Where(vc => vc.IsActive);
|
||||||
|
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +139,11 @@ public class ProductsController : Controller
|
|||||||
{
|
{
|
||||||
var categories = await _categoryService.GetAllCategoriesAsync();
|
var categories = await _categoryService.GetAllCategoriesAsync();
|
||||||
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
ViewData["Categories"] = categories.Where(c => c.IsActive);
|
||||||
|
|
||||||
|
// FIX: Re-populate VariantCollections for view rendering when validation fails
|
||||||
|
var variantCollections = await _variantCollectionService.GetAllVariantCollectionsAsync();
|
||||||
|
ViewData["VariantCollections"] = variantCollections.Where(vc => vc.IsActive);
|
||||||
|
|
||||||
ViewData["ProductId"] = id;
|
ViewData["ProductId"] = id;
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -180,7 +180,7 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
|
<form asp-area="Admin" asp-controller="Bots" asp-action="CheckRemoteStatus" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<button type="submit" class="btn btn-sm btn-outline-info">
|
<button type="submit" class="btn btn-sm btn-outline-info">
|
||||||
@ -195,6 +195,37 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (Model.DiscoveryStatus == "Configured")
|
||||||
|
{
|
||||||
|
<hr />
|
||||||
|
<h6 class="mb-2"><i class="fas fa-sliders-h"></i> Bot Control</h6>
|
||||||
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="StartBot" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-success">
|
||||||
|
<i class="fas fa-play"></i> Start
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="StopBot" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to stop the bot? Users will not be able to interact with it.')">
|
||||||
|
<i class="fas fa-stop"></i> Stop
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form asp-area="Admin" asp-controller="Bots" asp-action="RestartBot" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-warning">
|
||||||
|
<i class="fas fa-redo"></i> Restart
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted mt-2 d-block">
|
||||||
|
<i class="fas fa-info-circle"></i> Controls the Telegram polling connection on the remote bot instance.
|
||||||
|
</small>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -331,6 +362,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<form action="/Admin/Bots/Delete/@Model.Id" method="post">
|
<form action="/Admin/Bots/Delete/@Model.Id" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
<button type="submit" class="btn btn-danger w-100"
|
<button type="submit" class="btn btn-danger w-100"
|
||||||
onclick="return confirm('Are you sure you want to delete this bot? This action cannot be undone.')">
|
onclick="return confirm('Are you sure you want to delete this bot? This action cannot be undone.')">
|
||||||
<i class="bi bi-trash"></i> Delete Bot
|
<i class="bi bi-trash"></i> Delete Bot
|
||||||
|
|||||||
@ -37,15 +37,17 @@
|
|||||||
The TeleBot must be running and configured with the same discovery secret.
|
The TeleBot must be running and configured with the same discovery secret.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="ProbeRemote" method="post">
|
<form action="/Admin/Bots/ProbeRemote" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label for="IpAddress" class="form-label">IP Address / Hostname</label>
|
<label for="IpAddress" class="form-label">IP Address / Hostname</label>
|
||||||
<input name="IpAddress" id="IpAddress" value="@Model.IpAddress" class="form-control"
|
<input name="IpAddress" id="IpAddress" value="@Model.IpAddress" class="form-control"
|
||||||
placeholder="e.g., 192.168.1.100 or telebot.example.com" required />
|
placeholder="e.g., telebot, 193.233.245.41, or telebot.example.com" required />
|
||||||
<small class="text-muted">The IP address or hostname where TeleBot is running</small>
|
<small class="text-muted">
|
||||||
|
Use <code>telebot</code> for same-server Docker deployments, or the public IP/hostname for remote servers
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="Port" class="form-label">Port</label>
|
<label for="Port" class="form-label">Port</label>
|
||||||
@ -107,7 +109,7 @@
|
|||||||
<h5 class="mb-0"><i class="fas fa-robot"></i> Step 2: Register Bot</h5>
|
<h5 class="mb-0"><i class="fas fa-robot"></i> Step 2: Register Bot</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="RegisterRemote" method="post">
|
<form action="/Admin/Bots/RegisterRemote" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<!-- Hidden fields to preserve discovery data -->
|
<!-- Hidden fields to preserve discovery data -->
|
||||||
@ -188,7 +190,7 @@
|
|||||||
Now enter the Telegram bot token from BotFather to activate this bot.
|
Now enter the Telegram bot token from BotFather to activate this bot.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form asp-area="Admin" asp-controller="Bots" asp-action="ConfigureRemote" method="post">
|
<form action="/Admin/Bots/ConfigureRemote" method="post">
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
|
|
||||||
<!-- Hidden fields -->
|
<!-- Hidden fields -->
|
||||||
|
|||||||
@ -144,3 +144,22 @@ public static class DiscoveryStatus
|
|||||||
public const string Offline = "Offline";
|
public const string Offline = "Offline";
|
||||||
public const string Error = "Error";
|
public const string Error = "Error";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a bot control action (start/stop/restart)
|
||||||
|
/// </summary>
|
||||||
|
public class BotControlResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current bot status after the action
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether Telegram polling is currently running
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRunning { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@ -369,13 +369,127 @@ public class BotDiscoveryService : IBotDiscoveryService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<BotControlResult> ControlBotAsync(Guid botId, string action)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Sending control action '{Action}' to bot {BotId}", action, botId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get the bot details
|
||||||
|
var bot = await _botService.GetBotByIdAsync(botId);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Bot not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(bot.RemoteAddress) || !bot.RemotePort.HasValue)
|
||||||
|
{
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Bot does not have remote address configured. Control only works for remote bots."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the BotKey securely
|
||||||
|
var botKey = await _botService.GetBotKeyAsync(botId);
|
||||||
|
if (string.IsNullOrEmpty(botKey))
|
||||||
|
{
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Bot key not found"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint = BuildEndpoint(bot.RemoteAddress, bot.RemotePort.Value, "/api/discovery/control");
|
||||||
|
|
||||||
|
var payload = new { Action = action };
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
|
||||||
|
request.Headers.Add("X-Bot-Key", botKey);
|
||||||
|
request.Content = new StringContent(
|
||||||
|
JsonSerializer.Serialize(payload, JsonOptions),
|
||||||
|
Encoding.UTF8,
|
||||||
|
"application/json");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
var controlResponse = JsonSerializer.Deserialize<BotControlResult>(content, JsonOptions);
|
||||||
|
|
||||||
|
_logger.LogInformation("Bot control action '{Action}' completed for bot {BotId}: {Success}",
|
||||||
|
action, botId, controlResponse?.Success);
|
||||||
|
|
||||||
|
return controlResponse ?? new BotControlResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = $"Action '{action}' completed"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Invalid bot key. The bot may need to be re-initialized."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogWarning("Bot control action failed: {StatusCode} - {Content}",
|
||||||
|
response.StatusCode, errorContent);
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Control action failed: {response.StatusCode}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Bot control timed out for bot {BotId}", botId);
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Connection timed out. The bot may be offline."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Bot control connection failed for bot {BotId}", botId);
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Connection failed: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during bot control for bot {BotId}", botId);
|
||||||
|
return new BotControlResult
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Error: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region Private Methods
|
#region Private Methods
|
||||||
|
|
||||||
private string BuildEndpoint(string ipAddress, int port, string path)
|
private string BuildEndpoint(string ipAddress, int port, string path)
|
||||||
{
|
{
|
||||||
// Use HTTP for local/private networks, HTTPS for public
|
// Always use HTTP for discovery on custom ports
|
||||||
var scheme = IsPrivateNetwork(ipAddress) ? "http" : "https";
|
// HTTPS would require proper certificate setup which is unlikely on non-standard ports
|
||||||
return $"{scheme}://{ipAddress}:{port}{path}";
|
// If HTTPS is needed, the reverse proxy should handle SSL termination
|
||||||
|
return $"http://{ipAddress}:{port}{path}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPrivateNetwork(string ipAddress)
|
private bool IsPrivateNetwork(string ipAddress)
|
||||||
|
|||||||
@ -65,7 +65,7 @@ public class BotService : IBotService
|
|||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Name = dto.Name,
|
Name = dto.Name,
|
||||||
Description = dto.Description,
|
Description = dto.Description ?? string.Empty,
|
||||||
Type = dto.Type,
|
Type = dto.Type,
|
||||||
BotKey = botKey,
|
BotKey = botKey,
|
||||||
Status = BotStatus.Active,
|
Status = BotStatus.Active,
|
||||||
@ -158,6 +158,7 @@ public class BotService : IBotService
|
|||||||
var bots = await _context.Bots
|
var bots = await _context.Bots
|
||||||
.Include(b => b.Sessions)
|
.Include(b => b.Sessions)
|
||||||
.Include(b => b.Metrics)
|
.Include(b => b.Metrics)
|
||||||
|
.Where(b => b.Status != BotStatus.Deleted) // Filter out deleted bots
|
||||||
.OrderByDescending(b => b.CreatedAt)
|
.OrderByDescending(b => b.CreatedAt)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|||||||
@ -31,4 +31,11 @@ public interface IBotDiscoveryService
|
|||||||
/// Get the status of a remote TeleBot instance
|
/// Get the status of a remote TeleBot instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<DiscoveryProbeResponse?> GetRemoteStatusAsync(string ipAddress, int port, string botKey);
|
Task<DiscoveryProbeResponse?> GetRemoteStatusAsync(string ipAddress, int port, string botKey);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Control a remote TeleBot instance (start/stop/restart)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="botId">The bot ID in LittleShop</param>
|
||||||
|
/// <param name="action">Action to perform: "start", "stop", or "restart"</param>
|
||||||
|
Task<BotControlResult> ControlBotAsync(Guid botId, string action);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data Source=teleshop-production.db"
|
"DefaultConnection": "Data Source=/app/data/littleshop-production.db"
|
||||||
},
|
},
|
||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Key": "${JWT_SECRET_KEY}",
|
"Key": "${JWT_SECRET_KEY}",
|
||||||
|
|||||||
@ -219,6 +219,109 @@ public class DiscoveryController : ControllerBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Control the bot - start, stop, or restart Telegram polling
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("control")]
|
||||||
|
public async Task<IActionResult> Control([FromBody] BotControlRequest request)
|
||||||
|
{
|
||||||
|
// Require BotKey authentication for control actions
|
||||||
|
if (!ValidateBotKey())
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Bot control rejected: invalid or missing X-Bot-Key");
|
||||||
|
return Unauthorized(new { error = "Invalid bot key" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(request.Action))
|
||||||
|
{
|
||||||
|
return BadRequest(new BotControlResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Action is required (start, stop, restart)",
|
||||||
|
Status = _botManagerService.CurrentStatus,
|
||||||
|
IsRunning = _telegramBotService.IsRunning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var action = request.Action.ToLower();
|
||||||
|
_logger.LogInformation("Bot control action requested: {Action}", action);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
string message;
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case "start":
|
||||||
|
if (_telegramBotService.IsRunning)
|
||||||
|
{
|
||||||
|
return Ok(new BotControlResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Bot is already running",
|
||||||
|
Status = _botManagerService.CurrentStatus,
|
||||||
|
IsRunning = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
success = await _telegramBotService.StartPollingAsync();
|
||||||
|
message = success ? "Bot started successfully" : "Failed to start bot";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
if (!_telegramBotService.IsRunning)
|
||||||
|
{
|
||||||
|
return Ok(new BotControlResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Bot is not running",
|
||||||
|
Status = _botManagerService.CurrentStatus,
|
||||||
|
IsRunning = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_telegramBotService.StopPolling();
|
||||||
|
success = true;
|
||||||
|
message = "Bot stopped successfully";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "restart":
|
||||||
|
success = await _telegramBotService.RestartPollingAsync();
|
||||||
|
message = success ? "Bot restarted successfully" : "Failed to restart bot";
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return BadRequest(new BotControlResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Unknown action: {action}. Valid actions: start, stop, restart",
|
||||||
|
Status = _botManagerService.CurrentStatus,
|
||||||
|
IsRunning = _telegramBotService.IsRunning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Bot control action '{Action}' completed: {Success}", action, success);
|
||||||
|
|
||||||
|
return Ok(new BotControlResponse
|
||||||
|
{
|
||||||
|
Success = success,
|
||||||
|
Message = message,
|
||||||
|
Status = _botManagerService.CurrentStatus,
|
||||||
|
IsRunning = _telegramBotService.IsRunning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error during bot control action '{Action}'", action);
|
||||||
|
return StatusCode(500, new BotControlResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = $"Error: {ex.Message}",
|
||||||
|
Status = _botManagerService.CurrentStatus,
|
||||||
|
IsRunning = _telegramBotService.IsRunning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool ValidateDiscoverySecret()
|
private bool ValidateDiscoverySecret()
|
||||||
{
|
{
|
||||||
var providedSecret = Request.Headers["X-Discovery-Secret"].ToString();
|
var providedSecret = Request.Headers["X-Discovery-Secret"].ToString();
|
||||||
|
|||||||
@ -135,3 +135,33 @@ public class BotStatusUpdate
|
|||||||
public DateTime LastActivityAt { get; set; }
|
public DateTime LastActivityAt { get; set; }
|
||||||
public Dictionary<string, object>? Metadata { get; set; }
|
public Dictionary<string, object>? Metadata { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to control the bot (start/stop/restart)
|
||||||
|
/// </summary>
|
||||||
|
public class BotControlRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Control action: "start", "stop", or "restart"
|
||||||
|
/// </summary>
|
||||||
|
public string Action { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response after a control action
|
||||||
|
/// </summary>
|
||||||
|
public class BotControlResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current bot status after the action
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether Telegram polling is currently running
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRunning { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@ -227,6 +227,125 @@ namespace TeleBot
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start Telegram polling (if not already running)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> StartPollingAsync()
|
||||||
|
{
|
||||||
|
if (_isRunning)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Bot polling is already running");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_currentBotToken))
|
||||||
|
{
|
||||||
|
_currentBotToken = _configuration["Telegram:BotToken"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_currentBotToken) || _currentBotToken == "YOUR_BOT_TOKEN_HERE")
|
||||||
|
{
|
||||||
|
_logger.LogError("Cannot start: No bot token configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create bot client with TOR support if enabled
|
||||||
|
var torEnabled = _configuration.GetValue<bool>("Privacy:EnableTor");
|
||||||
|
if (torEnabled)
|
||||||
|
{
|
||||||
|
var torSocksHost = _configuration.GetValue<string>("Privacy:TorSocksHost") ?? "127.0.0.1";
|
||||||
|
var torSocksPort = _configuration.GetValue<int>("Privacy:TorSocksPort", 9050);
|
||||||
|
var proxyUri = $"socks5://{torSocksHost}:{torSocksPort}";
|
||||||
|
|
||||||
|
var handler = new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
Proxy = new WebProxy(proxyUri)
|
||||||
|
{
|
||||||
|
BypassProxyOnLocal = false,
|
||||||
|
UseDefaultCredentials = false
|
||||||
|
},
|
||||||
|
UseProxy = true,
|
||||||
|
AllowAutoRedirect = false,
|
||||||
|
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
||||||
|
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpClient = new HttpClient(handler);
|
||||||
|
_botClient = new TelegramBotClient(_currentBotToken, httpClient);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_botClient = new TelegramBotClient(_currentBotToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var receiverOptions = new ReceiverOptions
|
||||||
|
{
|
||||||
|
AllowedUpdates = Array.Empty<UpdateType>(),
|
||||||
|
ThrowPendingUpdates = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_botClient.StartReceiving(
|
||||||
|
HandleUpdateAsync,
|
||||||
|
HandleErrorAsync,
|
||||||
|
receiverOptions,
|
||||||
|
cancellationToken: _cancellationTokenSource.Token
|
||||||
|
);
|
||||||
|
|
||||||
|
_isRunning = true;
|
||||||
|
|
||||||
|
var me = await _botClient.GetMeAsync();
|
||||||
|
_logger.LogInformation("Bot polling started: @{Username} ({Id})", me.Username, me.Id);
|
||||||
|
|
||||||
|
// Update message delivery service
|
||||||
|
if (_messageDeliveryService is MessageDeliveryService deliveryService)
|
||||||
|
{
|
||||||
|
deliveryService.SetBotClient(_botClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to start bot polling");
|
||||||
|
_isRunning = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop Telegram polling
|
||||||
|
/// </summary>
|
||||||
|
public void StopPolling()
|
||||||
|
{
|
||||||
|
if (!_isRunning)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Bot polling is not running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
_isRunning = false;
|
||||||
|
_logger.LogInformation("Bot polling stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restart Telegram polling
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RestartPollingAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Restarting bot polling...");
|
||||||
|
StopPolling();
|
||||||
|
|
||||||
|
// Brief pause to ensure clean shutdown
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
return await StartPollingAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdateBotTokenAsync(string newToken)
|
public async Task UpdateBotTokenAsync(string newToken)
|
||||||
{
|
{
|
||||||
// If bot wasn't started or token changed, start/restart
|
// If bot wasn't started or token changed, start/restart
|
||||||
|
|||||||
@ -92,7 +92,7 @@
|
|||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
"Http": {
|
"Http": {
|
||||||
"Url": "http://localhost:5010"
|
"Url": "http://0.0.0.0:5010"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,39 +5,49 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: TeleBot/TeleBot/Dockerfile
|
dockerfile: TeleBot/TeleBot/Dockerfile
|
||||||
image: telebot:latest
|
image: localhost:5000/telebot:latest
|
||||||
container_name: telebot
|
container_name: telebot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5010:5010" # TeleBot API/health endpoint
|
||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- ASPNETCORE_URLS=http://+:5010
|
||||||
- TelegramBot__BotToken=${BOT_TOKEN}
|
- TelegramBot__BotToken=${BOT_TOKEN}
|
||||||
- TelegramBot__WebhookUrl=${WEBHOOK_URL}
|
- TelegramBot__WebhookUrl=${WEBHOOK_URL}
|
||||||
- TelegramBot__UseWebhook=false
|
- TelegramBot__UseWebhook=false
|
||||||
- LittleShopApi__BaseUrl=http://littleshop:5000
|
- LittleShopApi__BaseUrl=http://teleshop:8080
|
||||||
- LittleShopApi__ApiKey=${LITTLESHOP_API_KEY}
|
- LittleShopApi__ApiKey=${LITTLESHOP_API_KEY}
|
||||||
- Logging__LogLevel__Default=Information
|
- Logging__LogLevel__Default=Information
|
||||||
- Logging__LogLevel__Microsoft=Warning
|
- Logging__LogLevel__Microsoft=Warning
|
||||||
- Logging__LogLevel__Microsoft.Hosting.Lifetime=Information
|
- Logging__LogLevel__Microsoft.Hosting.Lifetime=Information
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- /opt/telebot/logs:/app/logs
|
||||||
- ./data:/app/data
|
- /opt/telebot/data:/app/data
|
||||||
- ./image_cache:/app/image_cache
|
- /opt/telebot/image_cache:/app/image_cache
|
||||||
networks:
|
networks:
|
||||||
- littleshop-network
|
teleshop-network:
|
||||||
depends_on:
|
aliases:
|
||||||
- littleshop
|
- telebot
|
||||||
|
silverpay-network:
|
||||||
|
aliases:
|
||||||
|
- telebot
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pgrep", "-f", "dotnet.*TeleBot"]
|
test: ["CMD", "curl", "-f", "http://localhost:5010/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
logging:
|
||||||
littleshop:
|
driver: "json-file"
|
||||||
external: true
|
options:
|
||||||
name: littleshop
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
littleshop-network:
|
teleshop-network:
|
||||||
|
name: sysadmin_teleshop-network
|
||||||
|
external: true
|
||||||
|
silverpay-network:
|
||||||
|
name: silverdotpay_silverdotpay-network
|
||||||
external: true
|
external: true
|
||||||
name: littleshop-network
|
|
||||||
249
deploy-alexhost.sh
Normal file
249
deploy-alexhost.sh
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# AlexHost Deployment Script
|
||||||
|
# Usage: ./deploy-alexhost.sh [teleshop|telebot|all] [--no-cache]
|
||||||
|
#
|
||||||
|
# This script transfers source to AlexHost and builds Docker images natively
|
||||||
|
# on the server to ensure correct architecture (AMD64).
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - sshpass installed (for password-based SSH)
|
||||||
|
# - tar installed
|
||||||
|
# - Access to AlexHost server
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration - can be overridden by environment variables
|
||||||
|
ALEXHOST_IP="${ALEXHOST_IP:-193.233.245.41}"
|
||||||
|
ALEXHOST_USER="${ALEXHOST_USER:-sysadmin}"
|
||||||
|
ALEXHOST_PASS="${ALEXHOST_PASS:-}"
|
||||||
|
REGISTRY="${REGISTRY:-localhost:5000}"
|
||||||
|
|
||||||
|
# Check for required password
|
||||||
|
if [ -z "$ALEXHOST_PASS" ]; then
|
||||||
|
echo -e "${RED}Error: ALEXHOST_PASS environment variable is required${NC}"
|
||||||
|
echo "Set it with: export ALEXHOST_PASS='your-password'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
DEPLOY_DIR="/home/sysadmin/teleshop-source"
|
||||||
|
BUILD_DIR="/tmp/littleshop-build"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
DEPLOY_TARGET="${1:-all}"
|
||||||
|
NO_CACHE=""
|
||||||
|
if [[ "$2" == "--no-cache" ]] || [[ "$1" == "--no-cache" ]]; then
|
||||||
|
NO_CACHE="--no-cache"
|
||||||
|
if [[ "$1" == "--no-cache" ]]; then
|
||||||
|
DEPLOY_TARGET="all"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo -e "${BLUE} AlexHost Deployment Script${NC}"
|
||||||
|
echo -e "${BLUE} Target: ${DEPLOY_TARGET}${NC}"
|
||||||
|
echo -e "${BLUE} Server: ${ALEXHOST_IP}${NC}"
|
||||||
|
echo -e "${BLUE} Mode: Server-side build (AMD64)${NC}"
|
||||||
|
echo -e "${BLUE}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Function to run SSH commands with sudo
|
||||||
|
ssh_sudo() {
|
||||||
|
sshpass -p "$ALEXHOST_PASS" ssh -o StrictHostKeyChecking=no "$ALEXHOST_USER@$ALEXHOST_IP" "echo '$ALEXHOST_PASS' | sudo -S bash -c '$1'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to copy files to AlexHost
|
||||||
|
scp_file() {
|
||||||
|
sshpass -p "$ALEXHOST_PASS" scp -o StrictHostKeyChecking=no "$1" "$ALEXHOST_USER@$ALEXHOST_IP:$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Transfer source to server
|
||||||
|
transfer_source() {
|
||||||
|
echo -e "${YELLOW}=== Transferring source to AlexHost ===${NC}"
|
||||||
|
|
||||||
|
# Create tarball excluding unnecessary files
|
||||||
|
echo "Creating source tarball..."
|
||||||
|
tar -czf /tmp/littleshop-source.tar.gz \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='bin' \
|
||||||
|
--exclude='obj' \
|
||||||
|
--exclude='*.tar.gz' \
|
||||||
|
-C "$SCRIPT_DIR" .
|
||||||
|
|
||||||
|
echo "Source tarball size: $(ls -lh /tmp/littleshop-source.tar.gz | awk '{print $5}')"
|
||||||
|
|
||||||
|
# Transfer to server
|
||||||
|
echo "Transferring to AlexHost..."
|
||||||
|
scp_file "/tmp/littleshop-source.tar.gz" "/tmp/"
|
||||||
|
scp_file "docker-compose.alexhost.yml" "/tmp/"
|
||||||
|
|
||||||
|
# Extract on server
|
||||||
|
echo "Extracting on server..."
|
||||||
|
ssh_sudo "rm -rf $BUILD_DIR && mkdir -p $BUILD_DIR && cd $BUILD_DIR && tar -xzf /tmp/littleshop-source.tar.gz"
|
||||||
|
|
||||||
|
# Cleanup local
|
||||||
|
rm -f /tmp/littleshop-source.tar.gz
|
||||||
|
|
||||||
|
echo -e "${GREEN}Source transferred successfully!${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy TeleShop
|
||||||
|
deploy_teleshop() {
|
||||||
|
echo -e "${YELLOW}=== Building TeleShop on AlexHost ===${NC}"
|
||||||
|
|
||||||
|
ssh_sudo "
|
||||||
|
set -e
|
||||||
|
cd $BUILD_DIR
|
||||||
|
|
||||||
|
echo 'Building TeleShop image...'
|
||||||
|
docker build $NO_CACHE -t littleshop:latest -f Dockerfile . 2>&1 | tail -15
|
||||||
|
|
||||||
|
echo 'Tagging and pushing to local registry...'
|
||||||
|
docker tag littleshop:latest localhost:5000/littleshop:latest
|
||||||
|
docker push localhost:5000/littleshop:latest
|
||||||
|
|
||||||
|
echo 'Stopping existing container...'
|
||||||
|
docker stop teleshop 2>/dev/null || true
|
||||||
|
docker rm teleshop 2>/dev/null || true
|
||||||
|
|
||||||
|
echo 'Copying compose file...'
|
||||||
|
mkdir -p $DEPLOY_DIR
|
||||||
|
cp /tmp/docker-compose.alexhost.yml $DEPLOY_DIR/docker-compose.yml
|
||||||
|
|
||||||
|
echo 'Starting TeleShop...'
|
||||||
|
cd $DEPLOY_DIR
|
||||||
|
docker compose up -d teleshop
|
||||||
|
|
||||||
|
echo 'Waiting for health check...'
|
||||||
|
sleep 30
|
||||||
|
docker ps | grep teleshop
|
||||||
|
"
|
||||||
|
|
||||||
|
echo -e "${GREEN}TeleShop deployment complete!${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy TeleBot
|
||||||
|
deploy_telebot() {
|
||||||
|
echo -e "${YELLOW}=== Building TeleBot on AlexHost ===${NC}"
|
||||||
|
|
||||||
|
ssh_sudo "
|
||||||
|
set -e
|
||||||
|
cd $BUILD_DIR
|
||||||
|
|
||||||
|
echo 'Building TeleBot image...'
|
||||||
|
docker build $NO_CACHE -t telebot:latest -f Dockerfile.telebot . 2>&1 | tail -15
|
||||||
|
|
||||||
|
echo 'Tagging and pushing to local registry...'
|
||||||
|
docker tag telebot:latest localhost:5000/telebot:latest
|
||||||
|
docker push localhost:5000/telebot:latest
|
||||||
|
|
||||||
|
echo 'Stopping existing container...'
|
||||||
|
docker stop telebot 2>/dev/null || true
|
||||||
|
docker rm telebot 2>/dev/null || true
|
||||||
|
|
||||||
|
echo 'Copying compose file...'
|
||||||
|
mkdir -p $DEPLOY_DIR
|
||||||
|
cp /tmp/docker-compose.alexhost.yml $DEPLOY_DIR/docker-compose.yml 2>/dev/null || true
|
||||||
|
|
||||||
|
echo 'Starting TeleBot...'
|
||||||
|
cd $DEPLOY_DIR
|
||||||
|
docker compose up -d telebot
|
||||||
|
|
||||||
|
echo 'Waiting for startup...'
|
||||||
|
sleep 20
|
||||||
|
docker ps | grep telebot
|
||||||
|
"
|
||||||
|
|
||||||
|
echo -e "${GREEN}TeleBot deployment complete!${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify deployment
|
||||||
|
verify_deployment() {
|
||||||
|
echo -e "${YELLOW}=== Verifying Deployment ===${NC}"
|
||||||
|
|
||||||
|
ssh_exec "
|
||||||
|
echo ''
|
||||||
|
echo 'Container Status:'
|
||||||
|
sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'NAMES|teleshop|telebot'
|
||||||
|
echo ''
|
||||||
|
|
||||||
|
echo 'Testing TeleShop health...'
|
||||||
|
if curl -sf http://localhost:5100/health > /dev/null; then
|
||||||
|
echo 'TeleShop: OK'
|
||||||
|
else
|
||||||
|
echo 'TeleShop: FAIL or starting...'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ''
|
||||||
|
echo 'Testing TeleBot...'
|
||||||
|
if sudo docker ps | grep -q 'telebot.*Up'; then
|
||||||
|
echo 'TeleBot: Running'
|
||||||
|
else
|
||||||
|
echo 'TeleBot: Not running'
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup build directory
|
||||||
|
cleanup() {
|
||||||
|
echo -e "${YELLOW}=== Cleaning up ===${NC}"
|
||||||
|
ssh_sudo "rm -rf $BUILD_DIR /tmp/littleshop-source.tar.gz"
|
||||||
|
echo -e "${GREEN}Cleanup complete${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
case "$DEPLOY_TARGET" in
|
||||||
|
teleshop)
|
||||||
|
transfer_source
|
||||||
|
deploy_teleshop
|
||||||
|
cleanup
|
||||||
|
verify_deployment
|
||||||
|
;;
|
||||||
|
telebot)
|
||||||
|
transfer_source
|
||||||
|
deploy_telebot
|
||||||
|
cleanup
|
||||||
|
verify_deployment
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
transfer_source
|
||||||
|
deploy_teleshop
|
||||||
|
deploy_telebot
|
||||||
|
cleanup
|
||||||
|
verify_deployment
|
||||||
|
;;
|
||||||
|
verify)
|
||||||
|
verify_deployment
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Usage: $0 [teleshop|telebot|all|verify] [--no-cache]${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 all # Deploy both services"
|
||||||
|
echo " $0 teleshop # Deploy only TeleShop"
|
||||||
|
echo " $0 telebot # Deploy only TeleBot"
|
||||||
|
echo " $0 all --no-cache # Deploy both without Docker cache"
|
||||||
|
echo " $0 verify # Just verify current deployment"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
echo -e "${GREEN} Deployment Complete!${NC}"
|
||||||
|
echo -e "${GREEN}========================================${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Access points:"
|
||||||
|
echo " TeleShop Admin: https://teleshop.silentmary.mywire.org/Admin"
|
||||||
|
echo " TeleShop API: https://teleshop.silentmary.mywire.org/api"
|
||||||
|
echo " TeleBot API: http://${ALEXHOST_IP}:5010"
|
||||||
124
docker-compose.alexhost.yml
Normal file
124
docker-compose.alexhost.yml
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# AlexHost Deployment Configuration
|
||||||
|
# Server: 193.233.245.41 (alexhost.silentmary.mywire.org)
|
||||||
|
# Registry: localhost:5000
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
teleshop:
|
||||||
|
image: localhost:5000/littleshop:latest
|
||||||
|
container_name: teleshop
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5100:8080"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- ASPNETCORE_URLS=http://+:8080
|
||||||
|
- ConnectionStrings__DefaultConnection=Data Source=/app/data/littleshop-production.db
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
- Jwt__Key=ThisIsAVeryLongSecretKeyThatIsDefinitelyLongerThan32BytesForSure123456789ABCDEF
|
||||||
|
- Jwt__Issuer=LittleShop-Production
|
||||||
|
- Jwt__Audience=LittleShop-Production
|
||||||
|
- Jwt__ExpiryInHours=24
|
||||||
|
|
||||||
|
# SilverPay Configuration
|
||||||
|
- SilverPay__BaseUrl=http://silverdotpay-api:8080
|
||||||
|
- SilverPay__PublicUrl=https://pay.thebankofdebbie.giize.com
|
||||||
|
- SilverPay__ApiKey=7703aa7a62fa4b40a87e9cfd867f5407147515c0986116ea54fc00c0a0bc30d8
|
||||||
|
- SilverPay__WebhookSecret=Thefa1r1esd1d1twebhooks2024
|
||||||
|
- SilverPay__DefaultWebhookUrl=https://admin.thebankofdebbie.giize.com/api/orders/payments/webhook
|
||||||
|
- SilverPay__AllowUnsignedWebhooks=false
|
||||||
|
|
||||||
|
# Admin Credentials
|
||||||
|
- AdminUser__Username=admin
|
||||||
|
- AdminUser__Password=Thefa1r1esd1d1t
|
||||||
|
|
||||||
|
# WebPush Notifications
|
||||||
|
- WebPush__VapidPublicKey=BMc6fFJZ8oIQKQzcl3kMnP9tTsjrm3oI_VxLt3lAGYUMWGInzDKn7jqclEoZzjvXy1QXGFb3dIun8mVBwh-QuS4
|
||||||
|
- WebPush__VapidPrivateKey=dYuuagbz2CzCnPDFUpO_qkGLBgnN3MEFZQnjXNkc1MY
|
||||||
|
- WebPush__Subject=mailto:admin@thebankofdebbie.giize.com
|
||||||
|
|
||||||
|
# Bot Discovery Configuration
|
||||||
|
- BotDiscovery__SharedSecret=AlexHostDiscovery2025SecretKey
|
||||||
|
- BotDiscovery__WebhookSecret=AlexHostWebhook2025SecretKey
|
||||||
|
- BotDiscovery__LittleShopApiUrl=https://admin.thebankofdebbie.giize.com
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /opt/littleshop/data:/app/data
|
||||||
|
- /opt/littleshop/uploads:/app/wwwroot/uploads
|
||||||
|
- /opt/littleshop/logs:/app/logs
|
||||||
|
networks:
|
||||||
|
teleshop-network:
|
||||||
|
aliases:
|
||||||
|
- teleshop
|
||||||
|
- littleshop
|
||||||
|
silverpay-network:
|
||||||
|
aliases:
|
||||||
|
- teleshop
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
telebot:
|
||||||
|
image: localhost:5000/telebot:latest
|
||||||
|
container_name: telebot
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5010:5010"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- ASPNETCORE_URLS=http://+:5010
|
||||||
|
|
||||||
|
# LittleShop API Connection (internal network)
|
||||||
|
- LittleShop__ApiUrl=http://teleshop:8080
|
||||||
|
- LittleShop__UseTor=false
|
||||||
|
|
||||||
|
# Telegram Bot Token (set via environment or will be configured via discovery)
|
||||||
|
- Telegram__BotToken=${TELEGRAM_BOT_TOKEN:-}
|
||||||
|
|
||||||
|
# Discovery Configuration (must match TeleShop)
|
||||||
|
- Discovery__Secret=AlexHostDiscovery2025SecretKey
|
||||||
|
|
||||||
|
# Privacy Settings
|
||||||
|
- Privacy__EnableTor=false
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /opt/telebot/data:/app/data
|
||||||
|
- /opt/telebot/logs:/app/logs
|
||||||
|
- /opt/telebot/image_cache:/app/image_cache
|
||||||
|
networks:
|
||||||
|
teleshop-network:
|
||||||
|
aliases:
|
||||||
|
- telebot
|
||||||
|
silverpay-network:
|
||||||
|
depends_on:
|
||||||
|
teleshop:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:5010/health || pgrep -f 'dotnet.*TeleBot' > /dev/null"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 60s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
teleshop-network:
|
||||||
|
name: sysadmin_teleshop-network
|
||||||
|
external: true
|
||||||
|
silverpay-network:
|
||||||
|
name: silverdotpay_silverdotpay-network
|
||||||
|
external: true
|
||||||
@ -1,12 +1,12 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
littleshop:
|
teleshop:
|
||||||
image: localhost:5000/littleshop:latest
|
image: localhost:5000/littleshop:latest
|
||||||
container_name: littleshop-admin
|
container_name: teleshop
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:5100:8080" # Local only, BunkerWeb will proxy
|
- "5100:8080" # External access on port 5100
|
||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ASPNETCORE_URLS=http://+:8080 # CRITICAL: Must use URLS not HTTP_PORTS
|
- ASPNETCORE_URLS=http://+:8080 # CRITICAL: Must use URLS not HTTP_PORTS
|
||||||
@ -19,7 +19,7 @@ services:
|
|||||||
- Jwt__ExpiryInHours=24
|
- Jwt__ExpiryInHours=24
|
||||||
|
|
||||||
# SilverPay Configuration (pay.thebankofdebbie.giize.com)
|
# SilverPay Configuration (pay.thebankofdebbie.giize.com)
|
||||||
- SilverPay__BaseUrl=http://silverpay-api:8000 # Internal Docker network - correct port
|
- SilverPay__BaseUrl=http://silverdotpay-api:8080 # Internal Docker network via silverpay-network
|
||||||
- SilverPay__PublicUrl=https://pay.thebankofdebbie.giize.com
|
- SilverPay__PublicUrl=https://pay.thebankofdebbie.giize.com
|
||||||
- SilverPay__ApiKey=7703aa7a62fa4b40a87e9cfd867f5407147515c0986116ea54fc00c0a0bc30d8
|
- SilverPay__ApiKey=7703aa7a62fa4b40a87e9cfd867f5407147515c0986116ea54fc00c0a0bc30d8
|
||||||
- SilverPay__WebhookSecret=Thefa1r1esd1d1twebhooks2024
|
- SilverPay__WebhookSecret=Thefa1r1esd1d1twebhooks2024
|
||||||
@ -44,7 +44,13 @@ services:
|
|||||||
- /opt/littleshop/uploads:/app/wwwroot/uploads
|
- /opt/littleshop/uploads:/app/wwwroot/uploads
|
||||||
- /opt/littleshop/logs:/app/logs
|
- /opt/littleshop/logs:/app/logs
|
||||||
networks:
|
networks:
|
||||||
- littleshop-network # Shared network for container communication
|
teleshop-network:
|
||||||
|
aliases:
|
||||||
|
- teleshop
|
||||||
|
- littleshop
|
||||||
|
silverpay-network:
|
||||||
|
aliases:
|
||||||
|
- teleshop
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -58,5 +64,9 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
littleshop-network:
|
teleshop-network:
|
||||||
|
name: sysadmin_teleshop-network
|
||||||
|
external: true
|
||||||
|
silverpay-network:
|
||||||
|
name: silverdotpay_silverdotpay-network
|
||||||
external: true
|
external: true
|
||||||
@ -11,7 +11,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ASPNETCORE_URLS=http://+:5000
|
- ASPNETCORE_URLS=http://+:5000
|
||||||
- ConnectionStrings__DefaultConnection=Data Source=/app/data/teleshop-prod.db
|
- ConnectionStrings__DefaultConnection=Data Source=/app/data/littleshop-production.db
|
||||||
- Jwt__Key=LittleShop-Production-JWT-SecretKey-32Characters-2025
|
- Jwt__Key=LittleShop-Production-JWT-SecretKey-32Characters-2025
|
||||||
- Jwt__Issuer=LittleShop
|
- Jwt__Issuer=LittleShop
|
||||||
- Jwt__Audience=LittleShop
|
- Jwt__Audience=LittleShop
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user