Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 112 additions & 52 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,30 +99,21 @@ jobs:
images: ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}

- name: Build and push client image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/client:latest
target: client

- name: Build and push server image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:latest
target: server

- name: Copy Nginx config to VPS
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USERNAME }}
key: ${{ secrets.VPS_SSH_KEY }}
source: "nginx/hackops.dracodev.me.conf"
target: "/tmp"

- name: Deploy to VPS
uses: appleboy/ssh-action@master
with:
Expand All @@ -144,56 +135,125 @@ jobs:
echo ' - "3000:3000"' >> docker-compose.yml
echo ' environment:' >> docker-compose.yml
echo ' - NODE_ENV=production' >> docker-compose.yml
echo ' - VITE_SERVER_URL=https://hackops.dracodev.me' >> docker-compose.yml
echo ' depends_on:' >> docker-compose.yml
echo ' - server' >> docker-compose.yml
echo ' restart: unless-stopped' >> docker-compose.yml
echo ' networks:' >> docker-compose.yml
echo ' - app-network' >> docker-compose.yml
echo '' >> docker-compose.yml
echo ' server:' >> docker-compose.yml
echo ' image: ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:latest' >> docker-compose.yml
echo ' ports:' >> docker-compose.yml
echo ' - "3001:3001"' >> docker-compose.yml
echo ' environment:' >> docker-compose.yml
echo ' - NODE_ENV=production' >> docker-compose.yml
echo ' - CORS_ORIGIN=https://hackops.dracodev.me' >> docker-compose.yml
echo ' command: node /app/server/index.js' >> docker-compose.yml
echo ' restart: unless-stopped' >> docker-compose.yml

# Pull latest images and restart containers
docker pull ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/client:latest
docker pull ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:latest
docker-compose down
docker-compose up -d

# Set up Nginx
# Install Nginx if not already installed
if ! command -v nginx &> /dev/null; then
apt-get update
apt-get install -y nginx
fi

# Copy Nginx configuration
cp /tmp/nginx/hackops.dracodev.me.conf /etc/nginx/sites-available/hackops.dracodev.me

# Create symbolic link if it doesn't exist
if [ ! -f /etc/nginx/sites-enabled/hackops.dracodev.me ]; then
ln -s /etc/nginx/sites-available/hackops.dracodev.me /etc/nginx/sites-enabled/
fi

# Remove default site if it exists
if [ -f /etc/nginx/sites-enabled/default ]; then
rm /etc/nginx/sites-enabled/default
fi

# Test Nginx configuration
nginx -t

# Reload Nginx
systemctl reload nginx

# Install Certbot for SSL if not already installed
if ! command -v certbot &> /dev/null; then
apt-get update
apt-get install -y certbot python3-certbot-nginx
fi

# Set up SSL certificate
certbot --nginx -d hackops.dracodev.me --non-interactive --agree-tos --email [email protected]
echo ' networks:' >> docker-compose.yml
echo ' - app-network' >> docker-compose.yml
echo '' >> docker-compose.yml
echo 'networks:' >> docker-compose.yml
echo ' app-network:' >> docker-compose.yml
echo ' driver: bridge' >> docker-compose.yml

echo ' # Create a dummy tracing module for the server' >> docker-compose.yml
echo ' mkdir -p tracing' >> docker-compose.yml
echo ' echo 'export default {};' > tracing/index.js' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Create a Dockerfile for the server fix' >> docker-compose.yml
echo ' cat > Dockerfile.server-fix << 'DOCKERFILE'' >> docker-compose.yml
echo ' FROM ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:latest' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Copy the dummy tracing module' >> docker-compose.yml
echo ' COPY tracing /app/server/tracing' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Set the command' >> docker-compose.yml
echo ' CMD ["node", "/app/server/index.js"]' >> docker-compose.yml
echo ' DOCKERFILE' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Pull latest images' >> docker-compose.yml
echo ' docker pull ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/client:latest' >> docker-compose.yml
echo ' docker pull ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:latest' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Build the fixed server image' >> docker-compose.yml
echo ' docker build -t ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:fixed -f Dockerfile.server-fix .' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Update docker-compose.yml to use the fixed image' >> docker-compose.yml
echo ' sed -i 's|ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:latest|ghcr.io/${{ steps.lowercase.outputs.owner }}/${{ steps.lowercase.outputs.repo }}/server:fixed|g' docker-compose.yml' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Restart containers' >> docker-compose.yml
echo ' docker-compose down' >> docker-compose.yml
echo ' docker-compose up -d' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Get container IPs for Nginx configuration' >> docker-compose.yml
echo ' CLIENT_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' hackops-submission_client_1)' >> docker-compose.yml
echo ' SERVER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' hackops-submission_server_1)' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Create Nginx configuration' >> docker-compose.yml
echo ' cat > /etc/nginx/sites-available/hackops.dracodev.me << NGINX_CONF' >> docker-compose.yml
echo ' server {' >> docker-compose.yml
echo ' server_name hackops.dracodev.me;' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' location / {' >> docker-compose.yml
echo ' proxy_pass http://$CLIENT_IP:3000;' >> docker-compose.yml
echo ' proxy_http_version 1.1;' >> docker-compose.yml
echo ' proxy_set_header Upgrade \$http_upgrade;' >> docker-compose.yml
echo ' proxy_set_header Connection 'upgrade';' >> docker-compose.yml
echo ' proxy_set_header Host \$host;' >> docker-compose.yml
echo ' proxy_cache_bypass \$http_upgrade;' >> docker-compose.yml
echo ' }' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' location /api/ {' >> docker-compose.yml
echo ' proxy_pass http://$SERVER_IP:3001/api/;' >> docker-compose.yml
echo ' proxy_http_version 1.1;' >> docker-compose.yml
echo ' proxy_set_header Upgrade \$http_upgrade;' >> docker-compose.yml
echo ' proxy_set_header Connection 'upgrade';' >> docker-compose.yml
echo ' proxy_set_header Host \$host;' >> docker-compose.yml
echo ' proxy_set_header X-Real-IP \$remote_addr;' >> docker-compose.yml
echo ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' >> docker-compose.yml
echo ' proxy_set_header X-Forwarded-Proto \$scheme;' >> docker-compose.yml
echo ' proxy_cache_bypass \$http_upgrade;' >> docker-compose.yml
echo ' }' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' location /socket.io/ {' >> docker-compose.yml
echo ' proxy_pass http://$SERVER_IP:3001/socket.io/;' >> docker-compose.yml
echo ' proxy_http_version 1.1;' >> docker-compose.yml
echo ' proxy_set_header Upgrade \$http_upgrade;' >> docker-compose.yml
echo ' proxy_set_header Connection "upgrade";' >> docker-compose.yml
echo ' proxy_set_header Host \$host;' >> docker-compose.yml
echo ' proxy_set_header X-Real-IP \$remote_addr;' >> docker-compose.yml
echo ' proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;' >> docker-compose.yml
echo ' proxy_set_header X-Forwarded-Proto \$scheme;' >> docker-compose.yml
echo ' proxy_cache_bypass \$http_upgrade;' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # WebSocket specific settings' >> docker-compose.yml
echo ' proxy_buffering off;' >> docker-compose.yml
echo ' proxy_read_timeout 86400;' >> docker-compose.yml
echo ' proxy_send_timeout 86400;' >> docker-compose.yml
echo ' }' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' listen 443 ssl; # managed by Certbot' >> docker-compose.yml
echo ' ssl_certificate /etc/letsencrypt/live/hackops.dracodev.me/fullchain.pem; # managed by Certbot' >> docker-compose.yml
echo ' ssl_certificate_key /etc/letsencrypt/live/hackops.dracodev.me/privkey.pem; # managed by Certbot' >> docker-compose.yml
echo ' include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot' >> docker-compose.yml
echo ' ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot' >> docker-compose.yml
echo ' }' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' server {' >> docker-compose.yml
echo ' if (\$host = hackops.dracodev.me) {' >> docker-compose.yml
echo ' return 301 https://\$host\$request_uri;' >> docker-compose.yml
echo ' } # managed by Certbot' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' listen 80;' >> docker-compose.yml
echo ' server_name hackops.dracodev.me;' >> docker-compose.yml
echo ' return 404; # managed by Certbot' >> docker-compose.yml
echo ' }' >> docker-compose.yml
echo ' NGINX_CONF' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Test Nginx configuration' >> docker-compose.yml
echo ' nginx -t' >> docker-compose.yml
echo ' ' >> docker-compose.yml
echo ' # Reload Nginx' >> docker-compose.yml
echo ' systemctl reload nginx' >> docker-compose.yml
16 changes: 3 additions & 13 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,18 @@ services:
target: client
ports:
- "3000:3000"
environment:
- NODE_ENV=production
depends_on:
- server
environment:
- NODE_ENV=production

server:
build:
context: .
target: server
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3001/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
deploy:
resources:
limits:
memory: 512M
restart: unless-stopped
- PORT=3001
21 changes: 16 additions & 5 deletions nginx/hackops.dracodev.me.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@ server {
proxy_cache_bypass $http_upgrade;
}

location /api {
proxy_pass http://localhost:3001;
location /api/ {
proxy_pass http://localhost:3001/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}

location /socket.io {
proxy_pass http://localhost:3001;
location /socket.io/ {
proxy_pass http://localhost:3001/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;

# WebSocket specific settings
proxy_buffering off;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}
69 changes: 37 additions & 32 deletions packages/client/src/services/socketService.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,69 @@
import { io, Socket } from 'socket.io-client';

// Get the server URL from environment or use default
const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'http://localhost:3001';

class SocketService {
private socket: Socket | null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private eventHandlers: Record<string, Array<(...args: any[]) => void>> = {};
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;

connect(): void {
connect() {
if (this.socket) return;

// Use relative URL instead of hardcoded localhost
const socketUrl = '/';

this.socket = io(SERVER_URL);

this.socket = io(socketUrl, {
transports: ['websocket', 'polling'],
reconnectionAttempts: this.maxReconnectAttempts,
reconnectionDelay: 1000,
timeout: 10000
});

this.socket.on('connect', () => {
console.log('Socket connected:', this.socket?.id);
console.log('Socket connected');
this.reconnectAttempts = 0;
});

this.socket.on('connect_error', (error) => {
console.error('Socket connection error:', error);
this.reconnectAttempts++;

if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached, giving up');
this.socket?.disconnect();
}
});

// Re-register any existing event handlers after reconnection
Object.entries(this.eventHandlers).forEach(([event, handlers]) => {
handlers.forEach(handler => {
this.socket?.on(event, handler);
});

this.socket.on('disconnect', (reason) => {
console.log('Socket disconnected:', reason);
});
}

disconnect(): void {
if (!this.socket) return;

this.socket.disconnect();
this.socket = null;
console.log('Socket disconnected');
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event: string, callback: (...args: any[]) => void): () => void {
if (!this.eventHandlers[event]) {
this.eventHandlers[event] = [];
}

this.eventHandlers[event].push(callback);
on(event: string, callback: (...args: any[]) => void) {
if (!this.socket) this.connect();
this.socket?.on(event, callback);

return () => {
this.eventHandlers[event] = this.eventHandlers[event].filter(cb => cb !== callback);
this.socket?.off(event, callback);
};
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
emit(event: string, ...args: any[]): void {
this.socket?.emit(event, ...args);
emit(event: string, data: any) {
if (!this.socket) this.connect();
this.socket?.emit(event, data);
}

// Utility method to check if a todo ID is valid
isTodoValid(id: string): boolean {
return id !== undefined && typeof id === 'string' && id.length > 0;
return typeof id === 'string' && id.length > 0;
}
}

Expand Down
Loading