diff --git a/BAN_NOTIFICATION_FIX.md b/BAN_NOTIFICATION_FIX.md new file mode 100644 index 0000000..fce06ff --- /dev/null +++ b/BAN_NOTIFICATION_FIX.md @@ -0,0 +1,277 @@ +# Ban Notification Feature - Deployment Guide + +## Overview +Fixed the issue where banned users only saw a toast notification instead of being redirected to the banned page. Now when a user is banned from the admin panel, they are **immediately redirected** via WebSocket notification. + +## Problem +When an admin banned a user: +1. User's account was updated in database ✅ +2. User saw a toast notification ❌ (not helpful) +3. User could continue using the site until page refresh ❌ +4. `/api/auth/me` returned 403, preventing frontend from getting ban info ❌ + +## Solution +### 1. Real-time WebSocket Notifications +- Backend sends `account_banned` event to user's active WebSocket connection +- Frontend receives event and refreshes auth state +- User is immediately redirected to `/banned` page + +### 2. Allow Banned Users to Access `/api/auth/me` +- Modified `middleware/auth.js` to allow banned users to access `/api/auth/me` +- This lets the frontend fetch ban details and redirect properly +- All other endpoints remain blocked for banned users + +## Changes Made + +### Backend (`routes/admin-management.js`) +```javascript +// Added WebSocket import +import wsManager from "../utils/websocket.js"; + +// In ban handler, after saving user: +if (wsManager.isUserConnected(user.steamId)) { + if (banned) { + wsManager.sendToUser(user.steamId, { + type: "account_banned", + data: { + banned: true, + reason: user.ban.reason, + bannedAt: new Date(), + bannedUntil: user.ban.expires, + }, + timestamp: Date.now(), + }); + } else { + wsManager.sendToUser(user.steamId, { + type: "account_unbanned", + data: { banned: false }, + timestamp: Date.now(), + }); + } +} +``` + +### Backend (`middleware/auth.js`) +```javascript +// Allow banned users to access /api/auth/me +const url = request.url || ""; +const routeUrl = request.routeOptions?.url || ""; +const isAuthMeEndpoint = + url.includes("/auth/me") || + routeUrl === "/me" || + routeUrl.endsWith("/me"); + +if (!isAuthMeEndpoint) { + // Block access to all other endpoints + return reply.status(403).send({ /* ban error */ }); +} +// If it's /api/auth/me, continue and attach user with ban info +``` + +### Frontend (`frontend/src/stores/websocket.js`) +```javascript +case "account_banned": + console.log("🔨 User account has been banned:", payload); + // Update the auth store - router guard will handle redirect + authStore.fetchUser().then(() => { + // Disconnect WebSocket since user is banned + disconnect(); + // The router guard will automatically redirect to /banned page + window.location.href = "/banned"; + }); + break; + +case "account_unbanned": + console.log("✅ User account has been unbanned:", payload); + // Update the auth store to reflect unbanned status + authStore.fetchUser().then(() => { + window.location.href = "/"; + toast.success("Your account has been unbanned. Welcome back!"); + }); + break; +``` + +## How It Works + +### Ban Flow +1. **Admin bans user** → Clicks "Ban" button in admin panel +2. **Backend saves ban** → Updates User record in MongoDB +3. **WebSocket notification** → Server sends `account_banned` event +4. **Frontend receives** → WebSocket store handles the event +5. **Auth refresh** → Calls `/api/auth/me` (now allowed for banned users) +6. **Router guard** → Sees `authStore.isBanned = true` +7. **Redirect** → User sent to `/banned` page immediately + +### Unban Flow +1. **Admin unbans user** → Clicks "Unban" button +2. **Backend clears ban** → Removes ban from User record +3. **WebSocket notification** → Server sends `account_unbanned` event +4. **Frontend receives** → Refreshes auth and redirects to home +5. **Success toast** → "Your account has been unbanned. Welcome back!" + +## Testing + +### Test Ban Notification +1. Open browser window as User A (logged in) +2. Open another window as Admin +3. Admin bans User A from admin panel +4. User A should **immediately** be redirected to `/banned` page +5. `/banned` page shows: + - Account suspended message + - Ban reason + - Ban duration (or "permanent") + - Contact support button + - Logout button + +### Test Unban Notification +1. User A is on `/banned` page +2. Admin unbans User A +3. User A should **immediately** be redirected to home page +4. Success toast appears + +### Test Without WebSocket +1. User A is logged in but WebSocket disconnected +2. Admin bans User A +3. User A continues using site temporarily +4. On next page navigation, router guard catches ban +5. User A redirected to `/banned` page + +## Deployment Steps + +### 1. Deploy Backend +```bash +# SSH to production server +ssh user@api.turbotrades.dev + +# Navigate to project +cd /path/to/TurboTrades + +# Pull latest changes (if using git) +git pull + +# Or manually upload files: +# - routes/admin-management.js +# - middleware/auth.js + +# Restart backend +pm2 restart turbotrades-backend + +# Verify backend is running +pm2 logs turbotrades-backend --lines 50 +``` + +### 2. Deploy Frontend +```bash +# On your local machine (Windows) +cd C:\Users\dg-ho\Documents\projects\TurboTrades\frontend +npm run build + +# On production server +cd /path/to/TurboTrades/frontend +npm run build +sudo cp -r dist/* /var/www/html/turbotrades/ + +# Verify files copied +ls -la /var/www/html/turbotrades/ +``` + +### 3. Test in Production +```bash +# Check backend logs for WebSocket connections +pm2 logs turbotrades-backend --lines 100 | grep WebSocket + +# Test ban flow +curl -X POST https://api.turbotrades.dev/api/admin/users/{userId}/ban \ + -H "Content-Type: application/json" \ + -H "Cookie: accessToken=YOUR_ADMIN_TOKEN" \ + -d '{"banned": true, "reason": "Test ban", "duration": 1}' +``` + +## Files Changed + +### Backend +- ✅ `routes/admin-management.js` - Added WebSocket ban notifications +- ✅ `middleware/auth.js` - Allow banned users to access `/api/auth/me` + +### Frontend +- ✅ `frontend/src/stores/websocket.js` - Handle ban/unban events +- ✅ `frontend/src/views/BannedPage.vue` - Already exists (no changes needed) +- ✅ `frontend/src/router/index.js` - Router guard already exists (no changes needed) + +## Logs to Watch + +### Backend +``` +📡 Sent ban notification to user {username} +📡 Sent unban notification to user {username} +🔨 Admin {admin} banned user {user} (Reason: {reason}) +``` + +### Frontend Console +``` +🔨 User account has been banned: {banInfo} +✅ User account has been unbanned: {unbanInfo} +🔵 Calling authStore.fetchUser() from WebSocket connected handler +``` + +## Troubleshooting + +### User not redirected after ban +1. Check if WebSocket is connected: `wsManager.isUserConnected(steamId)` +2. Check browser console for WebSocket messages +3. Verify `/api/auth/me` returns 200 with ban info (not 403) + +### 403 Error on `/api/auth/me` +1. Check `middleware/auth.js` deployed correctly +2. Verify URL matching logic: `url.includes("/auth/me")` +3. Backend logs should show: `✓ User authenticated: {username}` + +### Ban page not showing +1. Verify `authStore.isBanned` is true: Check console +2. Check router guard in `frontend/src/router/index.js` +3. Clear browser cache / hard refresh (Ctrl+Shift+R) + +### WebSocket not connected +1. Check WebSocket URL: `wss://api.turbotrades.dev/ws` +2. Verify Nginx WebSocket proxy configuration +3. Check backend WebSocket server is running + +## Security Notes + +✅ **Banned users can only access `/api/auth/me`** +- All other endpoints return 403 +- Prevents banned users from trading, depositing, etc. + +✅ **Admins cannot ban other admins** +- Check in ban handler: `user.staffLevel >= 3` + +✅ **WebSocket notifications only sent to connected users** +- Offline users will see ban on next page load via router guard + +## Next Steps + +- [ ] Add ban notification to email (optional) +- [ ] Log ban actions for audit trail (already logged to console) +- [ ] Add ban appeal form on `/banned` page (optional) +- [ ] Add bulk ban WebSocket notifications (if implementing bulk ban) + +## Related Files +- `frontend/src/views/BannedPage.vue` - The banned page UI +- `frontend/src/router/index.js` - Router guard for ban redirect +- `frontend/src/stores/auth.js` - Auth store with `isBanned` computed +- `routes/admin-management.js` - Admin ban/unban endpoints +- `middleware/auth.js` - Authentication middleware +- `utils/websocket.js` - WebSocket manager + +## Deployment Checklist + +- [ ] Backend code updated +- [ ] Frontend code updated +- [ ] Frontend built (`npm run build`) +- [ ] Backend restarted (`pm2 restart`) +- [ ] Frontend deployed to web root +- [ ] Tested ban flow (admin bans user → user redirected) +- [ ] Tested unban flow (admin unbans → user redirected) +- [ ] Verified `/api/auth/me` works for banned users +- [ ] Checked browser console for errors +- [ ] Checked backend logs for WebSocket messages \ No newline at end of file diff --git a/deploy-ban-fix.sh b/deploy-ban-fix.sh new file mode 100644 index 0000000..850da64 --- /dev/null +++ b/deploy-ban-fix.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Deploy Ban Notification Fix +# This script deploys the ban notification feature to production + +set -e # Exit on error + +echo "🚀 Deploying Ban Notification Fix..." +echo "" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +PROJECT_DIR="/path/to/TurboTrades" +FRONTEND_DIR="$PROJECT_DIR/frontend" +WEB_ROOT="/var/www/html/turbotrades" +PM2_APP_NAME="turbotrades-backend" + +# Check if running on production server +if [[ ! -d "$PROJECT_DIR" ]]; then + echo -e "${RED}❌ Error: Project directory not found at $PROJECT_DIR${NC}" + echo "Please update PROJECT_DIR in this script to match your server setup" + exit 1 +fi + +# Step 1: Backup current deployment +echo -e "${YELLOW}📦 Creating backup...${NC}" +BACKUP_DIR="$HOME/turbotrades-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$BACKUP_DIR" + +# Backup backend files +cp "$PROJECT_DIR/routes/admin-management.js" "$BACKUP_DIR/admin-management.js.bak" 2>/dev/null || true +cp "$PROJECT_DIR/middleware/auth.js" "$BACKUP_DIR/auth.js.bak" 2>/dev/null || true + +# Backup frontend +if [[ -d "$WEB_ROOT" ]]; then + cp -r "$WEB_ROOT" "$BACKUP_DIR/frontend" 2>/dev/null || true +fi + +echo -e "${GREEN}✓ Backup created at $BACKUP_DIR${NC}" +echo "" + +# Step 2: Pull latest changes (if using git) +cd "$PROJECT_DIR" +if [[ -d ".git" ]]; then + echo -e "${YELLOW}📥 Pulling latest changes from git...${NC}" + git pull + echo -e "${GREEN}✓ Git pull complete${NC}" + echo "" +else + echo -e "${YELLOW}⚠️ Not a git repository, skipping git pull${NC}" + echo -e "${YELLOW} Make sure you've manually uploaded the updated files:${NC}" + echo " - routes/admin-management.js" + echo " - middleware/auth.js" + echo " - frontend/src/stores/websocket.js" + echo "" + read -p "Press Enter to continue after uploading files..." +fi + +# Step 3: Install/update dependencies (if needed) +echo -e "${YELLOW}📦 Checking dependencies...${NC}" +cd "$PROJECT_DIR" +if [[ -f "package.json" ]]; then + npm install --production 2>/dev/null || echo "Dependencies already up to date" +fi +echo -e "${GREEN}✓ Dependencies checked${NC}" +echo "" + +# Step 4: Build frontend +echo -e "${YELLOW}🔨 Building frontend...${NC}" +cd "$FRONTEND_DIR" +npm run build + +if [[ ! -d "dist" ]]; then + echo -e "${RED}❌ Error: Frontend build failed (dist directory not found)${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Frontend built successfully${NC}" +echo "" + +# Step 5: Deploy frontend +echo -e "${YELLOW}🚀 Deploying frontend...${NC}" +if [[ ! -d "$WEB_ROOT" ]]; then + echo "Creating web root directory..." + sudo mkdir -p "$WEB_ROOT" +fi + +sudo cp -r dist/* "$WEB_ROOT/" +echo -e "${GREEN}✓ Frontend deployed to $WEB_ROOT${NC}" +echo "" + +# Step 6: Restart backend +echo -e "${YELLOW}🔄 Restarting backend...${NC}" +cd "$PROJECT_DIR" + +# Check if PM2 is available +if command -v pm2 &> /dev/null; then + pm2 restart "$PM2_APP_NAME" + + # Wait a moment for restart + sleep 2 + + # Check if process is running + if pm2 list | grep -q "$PM2_APP_NAME"; then + echo -e "${GREEN}✓ Backend restarted successfully${NC}" + else + echo -e "${RED}❌ Warning: Backend may not be running${NC}" + echo "Check logs with: pm2 logs $PM2_APP_NAME" + fi +else + echo -e "${YELLOW}⚠️ PM2 not found, please restart backend manually${NC}" +fi +echo "" + +# Step 7: Verify deployment +echo -e "${YELLOW}🔍 Verifying deployment...${NC}" + +# Check if backend is responding +BACKEND_URL="http://localhost:3000/api/health" +if curl -s "$BACKEND_URL" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Backend is responding${NC}" +else + echo -e "${RED}❌ Warning: Backend health check failed${NC}" +fi + +# Check if frontend files exist +if [[ -f "$WEB_ROOT/index.html" ]]; then + echo -e "${GREEN}✓ Frontend files deployed${NC}" +else + echo -e "${RED}❌ Warning: Frontend index.html not found${NC}" +fi + +echo "" + +# Step 8: Display logs +echo -e "${YELLOW}📋 Recent backend logs:${NC}" +if command -v pm2 &> /dev/null; then + pm2 logs "$PM2_APP_NAME" --lines 20 --nostream +else + echo "PM2 not available, skipping logs" +fi +echo "" + +# Success message +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${GREEN}✅ Ban Notification Fix Deployed Successfully!${NC}" +echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" +echo -e "${YELLOW}Next Steps:${NC}" +echo "1. Test the ban flow:" +echo " - Login as a user in one browser" +echo " - Login as admin in another browser" +echo " - Ban the user from admin panel" +echo " - User should be immediately redirected to /banned page" +echo "" +echo "2. Check the logs:" +echo " pm2 logs $PM2_APP_NAME --lines 50" +echo "" +echo "3. Monitor WebSocket connections:" +echo " pm2 logs $PM2_APP_NAME | grep WebSocket" +echo "" +echo -e "${YELLOW}Backup Location:${NC} $BACKUP_DIR" +echo "" +echo -e "${YELLOW}Rollback Instructions:${NC}" +echo "If something goes wrong, restore from backup:" +echo " cp $BACKUP_DIR/admin-management.js.bak $PROJECT_DIR/routes/admin-management.js" +echo " cp $BACKUP_DIR/auth.js.bak $PROJECT_DIR/middleware/auth.js" +echo " sudo cp -r $BACKUP_DIR/frontend/* $WEB_ROOT/" +echo " pm2 restart $PM2_APP_NAME" +echo "" +echo -e "${GREEN}Happy banning! 🔨${NC}" diff --git a/frontend/src/components/AdminConfigPanel.vue b/frontend/src/components/AdminConfigPanel.vue index a843a6d..cbda4cd 100644 --- a/frontend/src/components/AdminConfigPanel.vue +++ b/frontend/src/components/AdminConfigPanel.vue @@ -496,6 +496,80 @@

How often to auto-update market prices

+ +
+

💵 Instant Sell Payout

+
+ + +

+ Default percentage of market price paid to users (e.g., 70 = pay + 70% of item value) +

+
+
+ + +

+ Payout rate specifically for CS2 items (overrides global) +

+
+
+ + +

+ Payout rate specifically for Rust items (overrides global) +

+
+
+ + +

Minimum item value for instant sell

+
+
+ + +

Maximum item value for instant sell

+
+
@@ -970,6 +1044,22 @@ const promotionForm = ref({ code: "", }); +// Instant Sell form +const instantSellForm = ref({ + enabled: true, + payoutRate: 70.0, // Percentage (70 = 70%) + minItemValue: 0.01, + maxItemValue: 10000, + cs2: { + enabled: true, + payoutRate: 70.0, + }, + rust: { + enabled: true, + payoutRate: 70.0, + }, +}); + // Methods const loadConfig = async () => { loading.value = true; @@ -1016,6 +1106,25 @@ const loadConfig = async () => { commission: config.value.market.commission * 100, }; } + + if (config.value.instantSell) { + instantSellForm.value = { + enabled: config.value.instantSell.enabled ?? true, + // Convert decimal to percentage for display (0.7 -> 70) + payoutRate: (config.value.instantSell.payoutRate ?? 0.7) * 100, + minItemValue: config.value.instantSell.minItemValue ?? 0.01, + maxItemValue: config.value.instantSell.maxItemValue ?? 10000, + cs2: { + enabled: config.value.instantSell.cs2?.enabled ?? true, + payoutRate: (config.value.instantSell.cs2?.payoutRate ?? 0.7) * 100, + }, + rust: { + enabled: config.value.instantSell.rust?.enabled ?? true, + payoutRate: + (config.value.instantSell.rust?.payoutRate ?? 0.7) * 100, + }, + }; + } } } catch (error) { console.error("Failed to load config:", error); @@ -1060,25 +1169,46 @@ const saveAllSettings = async () => { commission: marketForm.value.commission / 100, // Convert percentage to decimal }; + const instantSellData = { + enabled: instantSellForm.value.enabled, + payoutRate: instantSellForm.value.payoutRate / 100, // Convert percentage to decimal + minItemValue: instantSellForm.value.minItemValue, + maxItemValue: instantSellForm.value.maxItemValue, + cs2: { + enabled: instantSellForm.value.cs2.enabled, + payoutRate: instantSellForm.value.cs2.payoutRate / 100, + }, + rust: { + enabled: instantSellForm.value.rust.enabled, + payoutRate: instantSellForm.value.rust.payoutRate / 100, + }, + }; + console.log("💾 Saving all settings..."); console.log("Trading data:", tradingData); console.log("Market data:", marketData); + console.log("Instant Sell data:", instantSellData); - // Save both in parallel - const [tradingResponse, marketResponse] = await Promise.all([ - axios.patch("/api/admin/config/trading", tradingData), - axios.patch("/api/admin/config/market", marketData), - ]); + // Save all in parallel + const [tradingResponse, marketResponse, instantSellResponse] = + await Promise.all([ + axios.patch("/api/admin/config/trading", tradingData), + axios.patch("/api/admin/config/market", marketData), + axios.patch("/api/admin/config/instantsell", instantSellData), + ]); - if (tradingResponse.data.success && marketResponse.data.success) { - toast.success("✅ All settings saved successfully!"); - await loadConfig(); + if ( + tradingResponse.data.success && + marketResponse.data.success && + instantSellResponse.data.success + ) { + toast.success("Settings saved successfully"); + await loadConfig(); // Reload to get updated values } else { - throw new Error("One or more settings failed to save"); + toast.error("Failed to save some settings"); } } catch (error) { - console.error("❌ Failed to save settings:", error); - console.error("Error response:", error.response?.data); + console.error("Failed to save settings:", error); toast.error(error.response?.data?.message || "Failed to save settings"); } finally { saving.value = false; @@ -1885,7 +2015,7 @@ onMounted(() => { .settings-grid { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(3, 1fr); gap: 2rem; margin-bottom: 2rem; } @@ -1986,6 +2116,12 @@ onMounted(() => { } } +@media (max-width: 1400px) { + .settings-grid { + grid-template-columns: repeat(2, 1fr); + } +} + @media (max-width: 1024px) { .settings-grid { grid-template-columns: 1fr; diff --git a/frontend/src/views/BannedPage.vue b/frontend/src/views/BannedPage.vue index 53c8748..0376c90 100644 --- a/frontend/src/views/BannedPage.vue +++ b/frontend/src/views/BannedPage.vue @@ -9,12 +9,6 @@

Account Suspended

- -

- Your account has been suspended due to a violation of our Terms of - Service. -

-