Compare commits

...

19 Commits

Author SHA1 Message Date
ae3fbe0261 added missing email verify page, corrected error
All checks were successful
Build Frontend / Build Frontend (push) Successful in 19s
2026-01-11 22:57:35 +00:00
5da4c0f34f added missing email verify page
All checks were successful
Build Frontend / Build Frontend (push) Successful in 21s
2026-01-11 22:50:15 +00:00
d736e9c69f changed smtp settings
All checks were successful
Build Frontend / Build Frontend (push) Successful in 9s
2026-01-11 21:59:48 +00:00
15e3eb316a changed smtp settings
All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
2026-01-11 21:52:31 +00:00
b05594fe3d update the ban page
All checks were successful
Build Frontend / Build Frontend (push) Successful in 8s
2026-01-11 04:44:30 +00:00
0f7f1ae5dd update the ban page
Some checks failed
Build Frontend / Build Frontend (push) Failing after 23s
2026-01-11 04:42:04 +00:00
87b290032c update the ban page
Some checks failed
Build Frontend / Build Frontend (push) Failing after 7s
2026-01-11 04:40:13 +00:00
af5c32561a update the ban page
All checks were successful
Build Frontend / Build Frontend (push) Successful in 23s
2026-01-11 04:36:42 +00:00
cbf50b0641 update the ban page
All checks were successful
Build Frontend / Build Frontend (push) Successful in 23s
2026-01-11 04:27:24 +00:00
5848323140 update the ban page
All checks were successful
Build Frontend / Build Frontend (push) Successful in 9s
2026-01-11 03:58:47 +00:00
2aff879291 added ban redirect correctly
All checks were successful
Build Frontend / Build Frontend (push) Successful in 25s
2026-01-11 03:55:47 +00:00
d794c5ad48 added ban redirect correctly
All checks were successful
Build Frontend / Build Frontend (push) Successful in 10s
2026-01-11 03:45:47 +00:00
5846541329 added ban redirect correctly
All checks were successful
Build Frontend / Build Frontend (push) Successful in 17s
2026-01-11 03:42:35 +00:00
aca0aca310 system now uses seperate pricing.
All checks were successful
Build Frontend / Build Frontend (push) Successful in 8s
2026-01-11 03:33:29 +00:00
02d9727a72 system now uses seperate pricing.
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
2026-01-11 03:31:54 +00:00
7a32454b83 system now uses seperate pricing.
All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
2026-01-11 03:24:54 +00:00
b686acee8f should fetch prices on load and every hour
All checks were successful
Build Frontend / Build Frontend (push) Successful in 23s
2026-01-11 03:08:16 +00:00
1f62e148e5 fixed routes
All checks were successful
Build Frontend / Build Frontend (push) Successful in 22s
2026-01-11 03:02:54 +00:00
ac72c6ad27 fixed routes 2026-01-11 03:01:12 +00:00
22 changed files with 2662 additions and 145 deletions

9
.env
View File

@@ -23,6 +23,7 @@ STEAM_RETURN_URL=https://api.turbotrades.dev/auth/steam/return
#Steam apis key - Loading the inventory
STEAM_APIS_KEY=DONTABUSEORPEPZWILLNAGASAKI
ENABLE_PRICE_UPDATES="true"
# Cookie Settings
COOKIE_DOMAIN=.turbotrades.dev
@@ -41,11 +42,11 @@ RATE_LIMIT_TIMEWINDOW=60000
BYPASS_BOT_REQUIREMENT=true
# Email Configuration (for future implementation)
SMTP_HOST=smtp.example.com
SMTP_HOST=mail.privateemail.com
SMTP_PORT=587
SMTP_USER=your-email@example.com
SMTP_PASS=your-email-password
EMAIL_FROM=noreply@turbotrades.com
SMTP_USER=spam@turbotrades.gg
SMTP_PASS=20yBBj!0
EMAIL_FROM=noreply@turbotrades.gg
# WebSocket
WS_PING_INTERVAL=30000

277
BAN_NOTIFICATION_FIX.md Normal file
View File

@@ -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

288
FINAL_DEPLOY_STEPS.md Normal file
View File

@@ -0,0 +1,288 @@
# 🚀 Final Deployment Steps - Steam Login & Navbar Fix
## 📋 What Was Fixed
1. **WebSocket Auth Sync**: WebSocket now triggers auth state refresh when connected
2. **Axios Configuration**: Fixed to use correct custom axios instance with proper baseURL
3. **API URL Detection**: Smart detection of API domain in production vs development
---
## 🎯 Deploy to Production Server
### Step 1: SSH into Your Server
```bash
ssh your-user@your-server
```
### Step 2: Navigate to Project & Pull Changes
```bash
cd /path/to/TurboTrades
git pull origin main
```
### Step 3: Build Frontend
```bash
cd frontend
npm install # only if package.json changed
npm run build
```
**Expected output:**
```
✓ 1588 modules transformed.
✓ built in 2.84s
```
### Step 4: Deploy to Web Root
```bash
# Copy built files to your web server directory
sudo cp -r dist/* /var/www/html/turbotrades/
# Verify files were copied
ls -la /var/www/html/turbotrades/
```
### Step 5: Set Yourself as Admin
```bash
# Go back to project root
cd /path/to/TurboTrades
# Run the make-admin script
node make-admin.js
```
**Expected output:**
```
✅ Connected to MongoDB
✅ Found user: ✅ Ashley アシュリー
Current staff level: 0
✅ Successfully updated user to admin (staffLevel: 3)
🎉 Done! Please restart your backend server.
```
### Step 6: Restart Backend (if needed)
```bash
pm2 restart turbotrades-backend
# Check logs
pm2 logs turbotrades-backend --lines 20
```
---
## 🧪 Test the Deployment
### 1. Clear Browser Cache
- **Chrome/Edge**: `Ctrl + Shift + Delete` → Clear all browsing data
- **Firefox**: `Ctrl + Shift + Delete` → Clear everything
- **Or use Incognito/Private mode**
### 2. Open Site & Console
1. Navigate to `https://turbotrades.dev`
2. Press `F12` to open DevTools
3. Go to **Console** tab
### 3. Clear Site Data
In DevTools:
- Go to **Application** tab
- Click **Clear storage**
- Click **Clear site data**
### 4. Check API Base URL
In the console, you should see:
```
🔵 Production API baseURL: https://api.turbotrades.dev
```
This confirms it's using the correct API domain.
### 5. Test Login Flow
1. Click **"Login to Steam"** button
2. Complete Steam authentication
3. You'll be redirected back to the site
### 6. Watch Console Logs
You should see this sequence:
```
🔵 Auth store initialize called - isInitialized: false
🔵 fetchUser called - fetching user from /api/auth/me
✅ fetchUser response: { success: true, user: {...} }
✅ Setting user in auth store: { steamId: "...", username: "...", ... }
🔵 fetchUser complete - isAuthenticated: true
Connecting to WebSocket: wss://api.turbotrades.dev/ws
WebSocket connected
🟢 WebSocket received 'connected' message: { ... }
🔵 Calling authStore.fetchUser() from WebSocket connected handler
🔵 fetchUser called - fetching user from /api/auth/me
✅ fetchUser response: { success: true, user: {...} }
✅ Setting user in auth store: { ... }
🔵 fetchUser complete - isAuthenticated: true
```
### 7. Verify Navbar Updates
The navbar should now show:
- ✅ Your Steam avatar
- ✅ Your username
- ✅ Your balance with green deposit button
- ✅ User dropdown menu with Profile, Inventory, Transactions, Withdraw
-**Admin** option (with yellow background) since you're admin
### 8. Test User API Endpoints
Click on **Profile** and verify:
- User stats load correctly
- No console errors about `/user/stats` 404
### 9. Test Admin Access
Click on **Admin** in the dropdown menu:
- You should be able to access the admin dashboard
- No permission errors
---
## ✅ Success Criteria
- [ ] Site loads without errors
- [ ] Console shows: `🔵 Production API baseURL: https://api.turbotrades.dev`
- [ ] Login button works
- [ ] After login, navbar shows logged-in state
- [ ] Avatar, username, and balance display correctly
- [ ] User dropdown menu works
- [ ] Profile page loads user stats
- [ ] Admin panel is accessible
- [ ] WebSocket connects successfully
- [ ] No 404 errors for `/api/user/*` routes
---
## 🐛 Troubleshooting
### Navbar Not Updating After Login
**Check console for errors:**
```
# If you see wrong API URL:
🔵 Development API baseURL: /api
```
**Solution:** Hard refresh with `Ctrl + Shift + R` to clear cached JS
---
### 404 Errors on /api/user/stats
**Check console:**
```
Failed to fetch user stats: 404
```
**Solution:** This should be fixed now, but if you still see it:
1. Verify backend is running: `pm2 status`
2. Check backend logs: `pm2 logs turbotrades-backend`
3. Verify routes are registered: `curl https://api.turbotrades.dev/api/health`
---
### WebSocket Not Connecting
**Check console:**
```
WebSocket error: ...
```
**Solution:**
```bash
# Check Nginx config
sudo cat /etc/nginx/sites-available/turbotrades-api | grep -A 5 "location /ws"
# Should have:
location /ws {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:3000/ws;
}
# Reload Nginx if changed
sudo nginx -t
sudo systemctl reload nginx
```
---
### Admin Panel Shows 403 Forbidden
**Check if admin was set correctly:**
```bash
# In your project directory
node -e "
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/turbotrades').then(async () => {
const User = mongoose.model('User', new mongoose.Schema({steamId: String, username: String, staffLevel: Number}));
const user = await User.findOne({steamId: '76561198027608071'});
console.log('User:', user.username, 'Staff Level:', user.staffLevel);
process.exit();
});
"
```
**Should output:**
```
User: ✅ Ashley アシュリー Staff Level: 3
```
If not 3, run `node make-admin.js` again.
---
## 📝 Files Changed
- `frontend/src/stores/websocket.js` - Added auth fetch on WebSocket connect
- `frontend/src/stores/auth.js` - Use custom axios instance, correct paths
- `frontend/src/utils/axios.js` - Smart API URL detection for production
---
## 🔄 Rollback (If Needed)
```bash
cd /path/to/TurboTrades
# Revert the last 3 commits
git revert HEAD~3..HEAD
git push origin main
# Rebuild and redeploy
cd frontend
npm run build
sudo cp -r dist/* /var/www/html/turbotrades/
```
---
## 🎉 You're Done!
After completing all steps:
1. ✅ Steam login works
2. ✅ Navbar updates after login
3. ✅ User API routes work
4. ✅ WebSocket connects and syncs auth
5. ✅ Admin panel is accessible
**Enjoy your fully functional TurboTrades platform!** 🚀

395
PRICING_SYSTEM_COMPLETE.md Normal file
View File

@@ -0,0 +1,395 @@
# TurboTrades Pricing & Payout System
## 📊 System Overview
TurboTrades has **two separate pricing systems** for different purposes:
### 1. **Marketplace** (User-to-User)
- Users list items at their own prices
- Other users buy from listings
- Site takes a commission (configurable in admin panel)
- Prices set by sellers
### 2. **Instant Sell** (User-to-Site)
- Site **buys items directly** from users
- Instant payout to user balance
- Price = Market Price × Payout Rate (e.g., 60%)
- Admin configures payout percentage
---
## 🗄️ Database Structure
### MarketPrice Collection (Reference Database)
- **Purpose**: Reference prices for ALL Steam market items
- **Updated**: Every 1 hour automatically
- **Source**: SteamAPIs.com market data
- **Used for**: Instant sell page pricing
- **Contents**: ~30,000+ items (CS2 + Rust)
### Item Collection (Marketplace Listings)
- **Purpose**: User-listed items for sale
- **Updated**: Every 1 hour (optional, for listed items)
- **Source**: User-set prices or suggested from MarketPrice
- **Used for**: Marketplace page
---
## ⚙️ How Instant Sell Works
### User Experience:
1. User goes to `/sell` page
2. Selects items from their Steam inventory
3. Sees **instant buy price** = Market Price × Payout Rate
4. Accepts offer
5. Site buys items, credits balance immediately
### Backend Flow:
1. **Fetch Inventory** (`GET /api/inventory/steam`)
- Gets user's Steam inventory
- Looks up each item in `MarketPrice` database
- Applies payout rate (e.g., 60%)
- Returns items with `marketPrice` (already discounted)
2. **Create Trade** (`POST /api/inventory/trade`)
- Validates items and prices
- Creates trade offer via Steam bot
- User accepts in Steam
- Balance credited on completion
### Example Calculation:
```
Market Price (MarketPrice DB): $10.00
Payout Rate (Admin Config): 60%
User Receives: $6.00
```
---
## 🔄 Automatic Price Updates
### On Server Startup:
```javascript
// Runs immediately when server starts
pricingService.updateAllPrices()
Updates MarketPrice database (CS2 + Rust)
Updates Item prices (marketplace listings)
```
### Every Hour:
```javascript
// Scheduled via setInterval (60 minutes)
pricingService.scheduleUpdates(60 * 60 * 1000)
updateMarketPriceDatabase('cs2') // ~20,000 items
updateMarketPriceDatabase('rust') // ~10,000 items
updateDatabasePrices('cs2') // Marketplace items
updateDatabasePrices('rust') // Marketplace items
```
### What Gets Updated:
-**MarketPrice**: ALL Steam market items
-**Item**: Only items listed on marketplace
- ⏱️ **Duration**: ~2-5 minutes per update
- 🔑 **Requires**: `STEAM_APIS_KEY` in `.env`
---
## 🎛️ Admin Configuration
### Instant Sell Settings
**Endpoint:** `PATCH /api/admin/config/instantsell`
**Settings Available:**
```json
{
"enabled": true, // Enable/disable instant sell
"payoutRate": 0.6, // 60% default payout
"minItemValue": 0.1, // Min $0.10
"maxItemValue": 10000, // Max $10,000
"cs2": {
"enabled": true,
"payoutRate": 0.65 // CS2-specific override (65%)
},
"rust": {
"enabled": true,
"payoutRate": 0.55 // Rust-specific override (55%)
}
}
```
### Setting Payout Rates
**Example: Set 65% payout for CS2, 55% for Rust**
```bash
curl -X PATCH https://api.turbotrades.dev/api/admin/config/instantsell \
-H "Content-Type: application/json" \
-H "Cookie: accessToken=YOUR_TOKEN" \
-d '{
"cs2": {
"payoutRate": 0.65
},
"rust": {
"payoutRate": 0.55
}
}'
```
**Example: Disable instant sell for Rust**
```bash
curl -X PATCH https://api.turbotrades.dev/api/admin/config/instantsell \
-H "Content-Type: application/json" \
-H "Cookie: accessToken=YOUR_TOKEN" \
-d '{
"rust": {
"enabled": false
}
}'
```
---
## 🚀 Initial Setup
### 1. Set API Key
```bash
# In your .env file
STEAM_APIS_KEY=your_api_key_here
```
Get API key from: https://steamapis.com/
### 2. Import Initial Prices (One-Time)
```bash
# Populates MarketPrice database
node import-market-prices.js
```
**Expected Output:**
```
📡 Fetching CS2 market data...
✅ Received 20,147 items from API
💾 Importing CS2 items to database...
✅ CS2 import complete: 20,147 inserted
📡 Fetching Rust market data...
✅ Received 9,823 items from API
💾 Importing Rust items to database...
✅ Rust import complete: 9,823 inserted
🎉 Total: 29,970 items imported
```
### 3. Configure Payout Rate
Use admin panel or API to set your desired payout percentage.
### 4. Start Server
```bash
pm2 start ecosystem.config.js --env production
```
Server will automatically:
- ✅ Update prices on startup
- ✅ Schedule hourly updates
- ✅ Keep prices fresh
---
## 📈 Price Update Logs
### Successful Update:
```
🔄 Starting price update for all games...
🔄 Updating MarketPrice reference database for CS2...
📡 Fetching market data from Steam API...
✅ Received 20,147 items from API
📦 Batch: 10,000 inserted, 10,147 updated
✅ MarketPrice update complete for CS2:
📥 Inserted: 234
🔄 Updated: 19,913
⏭️ Skipped: 0
🔄 Updating MarketPrice reference database for RUST...
✅ Received 9,823 items from API
✅ MarketPrice update complete for RUST:
📥 Inserted: 89
🔄 Updated: 9,734
✅ All price updates complete!
```
### View Logs:
```bash
pm2 logs turbotrades-backend --lines 100 | grep "price update"
```
---
## 🛠️ Manual Price Updates
### Update All Prices Now:
```bash
node update-prices-now.js
```
### Update Specific Game:
```bash
node update-prices-now.js cs2
node update-prices-now.js rust
```
### Check Price Coverage:
```bash
node check-prices.js
```
**Output:**
```
📊 Price Coverage Report
🎮 Counter-Strike 2:
Active Items: 1,245
With Prices: 1,198
Coverage: 96.2%
🔧 Rust:
Active Items: 387
With Prices: 375
Coverage: 96.9%
```
---
## 🔍 Troubleshooting
### Items Showing "Price unavailable" on Sell Page
**Cause:** MarketPrice database is empty or outdated
**Solution:**
```bash
# Import prices
node import-market-prices.js
# OR restart server (auto-updates on startup)
pm2 restart turbotrades-backend
```
### Prices Not Updating
**Check 1:** Verify API key
```bash
# Check if set
echo $STEAM_APIS_KEY
# Test API key
curl "https://api.steamapis.com/market/items/730?api_key=$STEAM_APIS_KEY" | head
```
**Check 2:** Check logs
```bash
pm2 logs turbotrades-backend --lines 200 | grep -E "price|update"
```
**Check 3:** Verify scheduler is running
```bash
# Should see this on startup:
⏰ Starting automatic price update scheduler...
🔄 Running initial price update on startup...
```
### Wrong Payout Rate Applied
**Check config:**
```bash
# Via API
curl https://api.turbotrades.dev/api/config/public | jq '.config.instantSell'
```
**Update config:**
```bash
# Via admin panel at /admin
# Or via API (requires admin auth)
```
---
## 📊 Database Queries
### Check MarketPrice Count:
```javascript
db.marketprices.countDocuments({ game: "cs2" })
db.marketprices.countDocuments({ game: "rust" })
```
### Find Item Price:
```javascript
db.marketprices.findOne({
marketHashName: "AK-47 | Redline (Field-Tested)",
game: "cs2"
})
```
### Most Expensive Items:
```javascript
db.marketprices.find({ game: "cs2" })
.sort({ price: -1 })
.limit(10)
```
### Last Update Time:
```javascript
db.marketprices.findOne({ game: "cs2" })
.sort({ lastUpdated: -1 })
.limit(1)
```
---
## 🎯 Best Practices
### Payout Rates:
- **Too High** (>80%): Site loses money
- **Too Low** (<40%): Users won't sell
- **Recommended**: 55-65% depending on competition
### Update Frequency:
- **Every Hour**: Good for active trading
- **Every 2-4 Hours**: Sufficient for most sites
- **Daily**: Only for low-volume sites
### Monitoring:
- Set up alerts for failed price updates
- Monitor price coverage percentage
- Track user complaints about prices
---
## 🔐 Security Notes
- ✅ API key stored in `.env` (never committed)
- ✅ Payout rate changes logged with admin username
- ✅ Rate limits on price update endpoints
- ✅ Validate prices before accepting trades
- ✅ Maximum item value limits prevent abuse
---
## 📝 Summary
| Feature | Status | Update Frequency |
|---------|--------|------------------|
| MarketPrice DB | ✅ Automatic | Every 1 hour |
| Item Prices | ✅ Automatic | Every 1 hour |
| Payout Rate | ⚙️ Admin Config | Manual |
| Initial Import | 🔧 Manual | One-time |
| Sell Page | ✅ Live | Real-time |
**Your instant sell page will now:**
- ✅ Show accurate prices from your database
- ✅ Apply your configured payout rate
- ✅ Update prices automatically every hour
- ✅ Allow per-game payout customization
**No more manual price updates needed!** 🎉

176
deploy-ban-fix.sh Normal file
View File

@@ -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}"

View File

@@ -496,6 +496,80 @@
<p class="form-help">How often to auto-update market prices</p>
</div>
</div>
<div class="settings-section">
<h3>💵 Instant Sell Payout</h3>
<div class="form-group">
<label>Global Payout Rate (%)</label>
<input
v-model.number="instantSellForm.payoutRate"
type="number"
step="0.1"
min="0"
max="100"
class="form-input"
placeholder="70.0"
/>
<p class="form-help">
Default percentage of market price paid to users (e.g., 70 = pay
70% of item value)
</p>
</div>
<div class="form-group">
<label>CS2 Payout Rate (%)</label>
<input
v-model.number="instantSellForm.cs2.payoutRate"
type="number"
step="0.1"
min="0"
max="100"
class="form-input"
placeholder="70.0"
/>
<p class="form-help">
Payout rate specifically for CS2 items (overrides global)
</p>
</div>
<div class="form-group">
<label>Rust Payout Rate (%)</label>
<input
v-model.number="instantSellForm.rust.payoutRate"
type="number"
step="0.1"
min="0"
max="100"
class="form-input"
placeholder="70.0"
/>
<p class="form-help">
Payout rate specifically for Rust items (overrides global)
</p>
</div>
<div class="form-group">
<label>Min Item Value ($)</label>
<input
v-model.number="instantSellForm.minItemValue"
type="number"
step="0.01"
min="0"
class="form-input"
placeholder="0.01"
/>
<p class="form-help">Minimum item value for instant sell</p>
</div>
<div class="form-group">
<label>Max Item Value ($)</label>
<input
v-model.number="instantSellForm.maxItemValue"
type="number"
step="1"
min="0"
class="form-input"
placeholder="10000"
/>
<p class="form-help">Maximum item value for instant sell</p>
</div>
</div>
</div>
<div class="form-actions-centered">
@@ -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([
// 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;

View File

@@ -0,0 +1,523 @@
<template>
<div class="space-y-6">
<!-- Header with Stats -->
<div class="bg-gray-800 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h2 class="text-2xl font-bold flex items-center gap-2">
<Package class="w-6 h-6 text-blue-400" />
Market Price Database
</h2>
<p class="text-gray-400 text-sm mt-1">
Manage reference prices for instant sell
</p>
</div>
<button
@click="refreshPrices"
:disabled="isRefreshing"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors flex items-center gap-2"
>
<RefreshCw class="w-4 h-4" :class="{ 'animate-spin': isRefreshing }" />
{{ isRefreshing ? 'Updating...' : 'Update All Prices' }}
</button>
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-gray-900 rounded-lg p-4">
<div class="text-gray-400 text-sm mb-1">Total Items</div>
<div class="text-2xl font-bold">{{ stats.total || 0 }}</div>
</div>
<div class="bg-gray-900 rounded-lg p-4">
<div class="text-gray-400 text-sm mb-1">CS2 Items</div>
<div class="text-2xl font-bold text-blue-400">{{ stats.cs2 || 0 }}</div>
</div>
<div class="bg-gray-900 rounded-lg p-4">
<div class="text-gray-400 text-sm mb-1">Rust Items</div>
<div class="text-2xl font-bold text-orange-400">{{ stats.rust || 0 }}</div>
</div>
<div class="bg-gray-900 rounded-lg p-4">
<div class="text-gray-400 text-sm mb-1">Last Updated</div>
<div class="text-lg font-semibold">
{{ stats.lastUpdated ? formatDate(stats.lastUpdated) : 'Never' }}
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-gray-800 rounded-lg p-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Search -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-400 mb-2">
Search Items
</label>
<div class="relative">
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
v-model="filters.search"
@input="debouncedSearch"
type="text"
placeholder="Search by name..."
class="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
/>
</div>
</div>
<!-- Game Filter -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
Game
</label>
<select
v-model="filters.game"
@change="loadItems"
class="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
>
<option value="">All Games</option>
<option value="cs2">CS2</option>
<option value="rust">Rust</option>
</select>
</div>
<!-- Sort -->
<div>
<label class="block text-sm font-medium text-gray-400 mb-2">
Sort By
</label>
<select
v-model="filters.sort"
@change="loadItems"
class="w-full px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="price-desc">Price (High to Low)</option>
<option value="price-asc">Price (Low to High)</option>
<option value="updated-desc">Recently Updated</option>
</select>
</div>
</div>
</div>
<!-- Items Table -->
<div class="bg-gray-800 rounded-lg overflow-hidden">
<div v-if="isLoading" class="p-8 text-center">
<RefreshCw class="w-8 h-8 animate-spin mx-auto mb-2 text-blue-400" />
<p class="text-gray-400">Loading items...</p>
</div>
<div v-else-if="items.length === 0" class="p-8 text-center">
<Package class="w-12 h-12 mx-auto mb-2 text-gray-600" />
<p class="text-gray-400">No items found</p>
</div>
<div v-else class="overflow-x-auto">
<table class="w-full">
<thead class="bg-gray-900 border-b border-gray-700">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Item
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Game
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Price
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Type
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
Last Updated
</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
<tr
v-for="item in items"
:key="item._id"
class="hover:bg-gray-700/50 transition-colors"
>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<img
v-if="item.image"
:src="item.image"
:alt="item.name"
class="w-12 h-12 rounded"
@error="(e) => (e.target.src = '/placeholder.png')"
/>
<div class="w-12 h-12 bg-gray-700 rounded flex items-center justify-center" v-else>
<Package class="w-6 h-6 text-gray-500" />
</div>
<div class="max-w-md">
<div class="font-medium text-white truncate">{{ item.name }}</div>
<div class="text-xs text-gray-500 truncate">{{ item.marketHashName }}</div>
</div>
</div>
</td>
<td class="px-6 py-4">
<span
class="px-2 py-1 text-xs font-semibold rounded"
:class="
item.game === 'cs2'
? 'bg-blue-900/50 text-blue-400'
: 'bg-orange-900/50 text-orange-400'
"
>
{{ item.game.toUpperCase() }}
</span>
</td>
<td class="px-6 py-4">
<div v-if="editingItem?._id === item._id" class="flex items-center gap-2">
<input
v-model.number="editingItem.price"
type="number"
step="0.01"
min="0"
class="w-24 px-2 py-1 bg-gray-900 border border-gray-700 rounded text-white text-sm"
/>
</div>
<div v-else class="font-semibold text-green-400">
{{ formatCurrency(item.price) }}
</div>
</td>
<td class="px-6 py-4">
<span class="text-sm text-gray-400">{{ item.priceType || 'safe' }}</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-400">
{{ formatDate(item.lastUpdated) }}
</div>
</td>
<td class="px-6 py-4 text-right">
<div v-if="editingItem?._id === item._id" class="flex items-center justify-end gap-2">
<button
@click="saveItem"
class="p-2 bg-green-600 hover:bg-green-700 rounded transition-colors"
title="Save"
>
<Check class="w-4 h-4" />
</button>
<button
@click="cancelEdit"
class="p-2 bg-gray-600 hover:bg-gray-700 rounded transition-colors"
title="Cancel"
>
<X class="w-4 h-4" />
</button>
</div>
<div v-else class="flex items-center justify-end gap-2">
<button
@click="editItem(item)"
class="p-2 bg-blue-600 hover:bg-blue-700 rounded transition-colors"
title="Edit Price"
>
<Edit class="w-4 h-4" />
</button>
<button
@click="deleteItem(item)"
class="p-2 bg-red-600 hover:bg-red-700 rounded transition-colors"
title="Delete"
>
<Trash2 class="w-4 h-4" />
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="px-6 py-4 bg-gray-900 border-t border-gray-700 flex items-center justify-between">
<div class="text-sm text-gray-400">
Showing {{ (pagination.page - 1) * pagination.limit + 1 }} to
{{ Math.min(pagination.page * pagination.limit, pagination.total) }} of
{{ pagination.total }} items
</div>
<div class="flex items-center gap-2">
<button
@click="goToPage(1)"
:disabled="pagination.page === 1"
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
>
<ChevronsLeft class="w-4 h-4" />
</button>
<button
@click="goToPage(pagination.page - 1)"
:disabled="pagination.page === 1"
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
>
<ChevronLeft class="w-4 h-4" />
</button>
<!-- Page Numbers -->
<div class="flex items-center gap-1">
<button
v-for="page in visiblePages"
:key="page"
@click="goToPage(page)"
class="px-3 py-1 rounded transition-colors"
:class="
page === pagination.page
? 'bg-blue-600 text-white'
: 'bg-gray-800 hover:bg-gray-700'
"
>
{{ page }}
</button>
</div>
<button
@click="goToPage(pagination.page + 1)"
:disabled="pagination.page >= pagination.pages"
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
>
<ChevronRight class="w-4 h-4" />
</button>
<button
@click="goToPage(pagination.pages)"
:disabled="pagination.page >= pagination.pages"
class="px-3 py-1 bg-gray-800 hover:bg-gray-700 disabled:bg-gray-800 disabled:text-gray-600 disabled:cursor-not-allowed rounded transition-colors"
>
<ChevronsRight class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import axios from '@/utils/axios';
import { useToast } from 'vue-toastification';
import {
Package,
RefreshCw,
Search,
Edit,
Trash2,
Check,
X,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from 'lucide-vue-next';
const toast = useToast();
// State
const items = ref([]);
const stats = ref({
total: 0,
cs2: 0,
rust: 0,
lastUpdated: null,
});
const isLoading = ref(false);
const isRefreshing = ref(false);
const editingItem = ref(null);
// Filters
const filters = ref({
search: '',
game: '',
sort: 'name-asc',
});
// Pagination
const pagination = ref({
page: 1,
limit: 50,
total: 0,
pages: 0,
});
// Computed
const visiblePages = computed(() => {
const current = pagination.value.page;
const total = pagination.value.pages;
const delta = 2;
const pages = [];
for (let i = Math.max(1, current - delta); i <= Math.min(total, current + delta); i++) {
pages.push(i);
}
return pages;
});
// Methods
const loadItems = async () => {
isLoading.value = true;
try {
const params = {
page: pagination.value.page,
limit: pagination.value.limit,
game: filters.value.game,
search: filters.value.search,
sort: filters.value.sort,
};
const response = await axios.get('/api/admin/marketprices', { params });
if (response.data.success) {
items.value = response.data.items || [];
pagination.value = {
page: response.data.page || 1,
limit: response.data.limit || 50,
total: response.data.total || 0,
pages: response.data.pages || 0,
};
}
} catch (error) {
console.error('Failed to load items:', error);
toast.error('Failed to load items');
} finally {
isLoading.value = false;
}
};
const loadStats = async () => {
try {
const response = await axios.get('/api/admin/marketprices/stats');
if (response.data.success) {
stats.value = response.data.stats;
}
} catch (error) {
console.error('Failed to load stats:', error);
}
};
const refreshPrices = async () => {
if (isRefreshing.value) return;
isRefreshing.value = true;
try {
const response = await axios.post('/api/admin/prices/update', {
game: 'all',
});
if (response.data.success) {
toast.success('Price update started! This may take a few minutes.');
// Reload after a delay
setTimeout(() => {
loadItems();
loadStats();
}, 5000);
}
} catch (error) {
console.error('Failed to refresh prices:', error);
toast.error('Failed to start price update');
} finally {
isRefreshing.value = false;
}
};
const editItem = (item) => {
editingItem.value = { ...item };
};
const cancelEdit = () => {
editingItem.value = null;
};
const saveItem = async () => {
if (!editingItem.value) return;
try {
const response = await axios.patch(
`/api/admin/marketprices/${editingItem.value._id}`,
{
price: editingItem.value.price,
}
);
if (response.data.success) {
toast.success('Price updated successfully');
// Update item in list
const index = items.value.findIndex((i) => i._id === editingItem.value._id);
if (index !== -1) {
items.value[index] = { ...editingItem.value };
}
editingItem.value = null;
}
} catch (error) {
console.error('Failed to update item:', error);
toast.error(error.response?.data?.message || 'Failed to update item');
}
};
const deleteItem = async (item) => {
if (!confirm(`Delete "${item.name}"? This action cannot be undone.`)) {
return;
}
try {
const response = await axios.delete(`/api/admin/marketprices/${item._id}`);
if (response.data.success) {
toast.success('Item deleted successfully');
loadItems();
loadStats();
}
} catch (error) {
console.error('Failed to delete item:', error);
toast.error(error.response?.data?.message || 'Failed to delete item');
}
};
const goToPage = (page) => {
if (page < 1 || page > pagination.value.pages) return;
pagination.value.page = page;
loadItems();
};
// Debounced search
let searchTimeout;
const debouncedSearch = () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
pagination.value.page = 1; // Reset to first page
loadItems();
}, 500);
};
// Formatting helpers
const formatCurrency = (value) => {
if (value === null || value === undefined) return 'N/A';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
};
const formatDate = (date) => {
if (!date) return 'N/A';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
// Lifecycle
onMounted(() => {
loadItems();
loadStats();
});
</script>
<style scoped>
/* Add any custom styles here */
</style>

View File

@@ -104,6 +104,12 @@ const routes = [
component: () => import("@/views/MaintenancePage.vue"),
meta: { title: "Maintenance Mode" },
},
{
path: "/verify-email/:token",
name: "MailVerify",
component: () => import("@/views/MailVerify.vue"),
meta: { title: "Verify Email" },
},
{
path: "/banned",
name: "Banned",

View File

@@ -47,7 +47,7 @@ export const useAuthStore = defineStore("auth", () => {
console.log("🔵 fetchUser called - fetching user from /api/auth/me");
isLoading.value = true;
try {
const response = await axios.get("/auth/me", {
const response = await axios.get("/api/auth/me", {
withCredentials: true,
});
@@ -86,7 +86,7 @@ export const useAuthStore = defineStore("auth", () => {
isLoading.value = true;
try {
await axios.post(
"/auth/logout",
"/api/auth/logout",
{},
{
withCredentials: true,
@@ -111,7 +111,7 @@ export const useAuthStore = defineStore("auth", () => {
const refreshToken = async () => {
try {
await axios.post(
"/auth/refresh",
"/api/auth/refresh",
{},
{
withCredentials: true,
@@ -129,7 +129,7 @@ export const useAuthStore = defineStore("auth", () => {
isLoading.value = true;
try {
const response = await axios.patch(
"/user/trade-url",
"/api/user/trade-url",
{ tradeUrl },
{ withCredentials: true }
);
@@ -155,7 +155,7 @@ export const useAuthStore = defineStore("auth", () => {
isLoading.value = true;
try {
const response = await axios.patch(
"/user/email",
"/api/user/email",
{ email },
{ withCredentials: true }
);
@@ -178,7 +178,7 @@ export const useAuthStore = defineStore("auth", () => {
const verifyEmail = async (token) => {
isLoading.value = true;
try {
const response = await axios.get(`/user/verify-email/${token}`, {
const response = await axios.get(`/api/user/verify-email/${token}`, {
withCredentials: true,
});
@@ -199,7 +199,7 @@ export const useAuthStore = defineStore("auth", () => {
const getUserStats = async () => {
try {
const response = await axios.get("/user/stats", {
const response = await axios.get("/api/user/stats", {
withCredentials: true,
});
@@ -215,7 +215,7 @@ export const useAuthStore = defineStore("auth", () => {
const getBalance = async () => {
try {
const response = await axios.get("/user/balance", {
const response = await axios.get("/api/user/balance", {
withCredentials: true,
});

View File

@@ -220,6 +220,28 @@ export const useWebSocketStore = defineStore("websocket", () => {
authStore.fetchUser();
break;
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
// because authStore.isBanned will now be true
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(() => {
// Redirect to home page
window.location.href = "/";
toast.success("Your account has been unbanned. Welcome back!");
});
break;
case "pong":
// Heartbeat response
break;

View File

@@ -10,7 +10,7 @@ const getApiBaseUrl = () => {
return import.meta.env.VITE_API_URL;
}
// In production, use the api subdomain
// In production, use the api subdomain (WITHOUT /api suffix - routes already have it)
if (import.meta.env.PROD) {
const currentHost = window.location.hostname;
const protocol = window.location.protocol;
@@ -20,13 +20,13 @@ const getApiBaseUrl = () => {
currentHost === "turbotrades.dev" ||
currentHost === "www.turbotrades.dev"
) {
const baseUrl = `${protocol}//api.turbotrades.dev/api`;
const baseUrl = `${protocol}//api.turbotrades.dev`;
console.log("🔵 Production API baseURL:", baseUrl);
return baseUrl;
}
// For other domains, try api subdomain
const baseUrl = `${protocol}//api.${currentHost}/api`;
const baseUrl = `${protocol}//api.${currentHost}`;
console.log("🔵 Production API baseURL (custom domain):", baseUrl);
return baseUrl;
}
@@ -37,8 +37,16 @@ const getApiBaseUrl = () => {
};
// Create axios instance
const baseURL = getApiBaseUrl();
console.log("🔧 Creating axios instance with baseURL:", baseURL);
console.log(
"🔧 Environment:",
import.meta.env.PROD ? "PRODUCTION" : "DEVELOPMENT"
);
console.log("🔧 Current hostname:", window.location.hostname);
const axiosInstance = axios.create({
baseURL: getApiBaseUrl(),
baseURL: baseURL,
timeout: 15000,
withCredentials: true,
headers: {
@@ -46,17 +54,22 @@ const axiosInstance = axios.create({
},
});
console.log("✅ Axios instance created with config:", {
baseURL: axiosInstance.defaults.baseURL,
withCredentials: axiosInstance.defaults.withCredentials,
timeout: axiosInstance.defaults.timeout,
});
// Request interceptor
axiosInstance.interceptors.request.use(
(config) => {
// You can add auth token to headers here if needed
// const token = localStorage.getItem('token')
// if (token) {
// config.headers.Authorization = `Bearer ${token}`
// }
// Log every request for debugging
const fullUrl = `${config.baseURL}${config.url}`;
console.log(`📤 Axios Request: ${config.method.toUpperCase()} ${fullUrl}`);
return config;
},
(error) => {
console.error("❌ Axios Request Error:", error);
return Promise.reject(error);
}
);

View File

@@ -477,7 +477,12 @@
</div>
<!-- Items Tab -->
<div v-if="activeTab === 'items'" class="space-y-6">
<div v-if="activeTab === 'items'">
<AdminItemsPanel />
</div>
<!-- Old Items Tab (keeping for reference, remove if not needed) -->
<div v-if="activeTab === 'items-old'" class="space-y-6">
<!-- Game Filter -->
<div class="flex gap-2">
<button
@@ -753,6 +758,7 @@ import axios from "../utils/axios";
import AdminUsersPanel from "../components/AdminUsersPanel.vue";
import AdminConfigPanel from "../components/AdminConfigPanel.vue";
import AdminDebugPanel from "../components/AdminDebugPanel.vue";
import AdminItemsPanel from "../components/AdminItemsPanel.vue";
import {
Shield,
RefreshCw,

View File

@@ -9,13 +9,8 @@
<!-- Title -->
<h1 class="banned-title">Account Suspended</h1>
<!-- Message -->
<p class="banned-message">
Your account has been suspended due to a violation of our Terms of
Service.
</p>
<!-- Ban Details -->
<!-- Ban Details and Appeal Section -->
<div class="ban-details-container">
<div v-if="banInfo" class="ban-details">
<div class="detail-item">
<span class="detail-label">Reason:</span>
@@ -24,37 +19,14 @@
}}</span>
</div>
<div v-if="banInfo.bannedAt" class="detail-item">
<span class="detail-label">Banned on:</span>
<span class="detail-value">{{ formatDate(banInfo.bannedAt) }}</span>
</div>
<div v-if="banInfo.bannedUntil" class="detail-item">
<span class="detail-label">Ban expires:</span>
<span class="detail-value">{{
formatDate(banInfo.bannedUntil)
}}</span>
</div>
<div v-else class="detail-item permanent-ban">
<AlertCircle :size="18" />
<span>This is a permanent ban</span>
</div>
</div>
<!-- Info Box -->
<div class="info-box">
<Info :size="20" class="info-icon" />
<div class="info-content">
<p class="info-title">What does this mean?</p>
<p class="info-text">
You will not be able to access your account, make trades, or use the
marketplace while your account is suspended.
</p>
</div>
</div>
<!-- Appeal Section -->
<div class="appeal-section">
<p class="appeal-text">
If you believe this ban was made in error, you can submit an appeal.
@@ -64,6 +36,7 @@
<span>Contact Support</span>
</a>
</div>
</div>
<!-- Logout Button -->
<button @click="handleLogout" class="logout-btn">
@@ -134,10 +107,7 @@ const banInfo = computed(() => {
return {
reason: authStore.user.ban?.reason,
bannedAt: authStore.user.ban?.bannedAt,
bannedUntil: authStore.user.ban?.bannedUntil,
isPermanent:
authStore.user.ban?.permanent || !authStore.user.ban?.bannedUntil,
bannedUntil: authStore.user.ban?.expires,
};
});
@@ -293,15 +263,6 @@ onMounted(() => {
line-height: 1.6;
}
.ban-details {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 1rem;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: left;
}
.detail-item {
display: flex;
justify-content: space-between;
@@ -329,14 +290,6 @@ onMounted(() => {
text-align: right;
}
.permanent-ban {
justify-content: center;
gap: 0.5rem;
color: #ef4444;
font-weight: 600;
font-size: 0.9375rem;
}
.info-box {
display: flex;
gap: 1rem;
@@ -372,23 +325,40 @@ onMounted(() => {
line-height: 1.5;
}
.appeal-section {
margin: 2rem 0;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
.ban-details-container {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 1.5rem;
margin-bottom: 2rem;
}
.ban-details {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
text-align: left;
}
.appeal-section {
margin: 0;
padding: 0;
background: none;
border: none;
text-align: center;
}
.appeal-text {
font-size: 0.9375rem;
color: #d1d5db;
margin: 0 0 1rem 0;
text-align: center;
}
.appeal-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
@@ -400,6 +370,8 @@ onMounted(() => {
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
border: 1px solid rgba(59, 130, 246, 0.5);
width: 100%;
max-width: 300px;
}
.appeal-btn:hover {
@@ -524,8 +496,7 @@ onMounted(() => {
}
.appeal-btn {
width: 100%;
justify-content: center;
max-width: 100%;
}
}
</style>

View File

@@ -0,0 +1,113 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import axios from "axios";
const router = useRouter();
const route = useRoute();
const loading = ref(true);
const success = ref(false);
const error = ref(false);
const message = ref("Verifying your email...");
const countdown = ref(5); // countdown in seconds
const goHome = () => router.push("/");
const goBack = () => router.back();
let countdownInterval = null;
onMounted(async () => {
const token = route.params.token;
if (!token) {
error.value = true;
loading.value = false;
message.value = "No verification token provided.";
return;
}
try {
const res = await axios.get(
`https://api.turbotrades.dev/api/user/verify-email/${token}`
);
if (res.data.success) {
success.value = true;
message.value = "Email verified successfully! Redirecting to homepage...";
loading.value = false;
// Start countdown
countdownInterval = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(countdownInterval);
router.push("/");
}
}, 1000);
} else {
error.value = true;
loading.value = false;
message.value = res.data.message || "Failed to verify email.";
}
} catch (err) {
error.value = true;
loading.value = false;
message.value =
err.response?.data?.message ||
"An error occurred while verifying your email.";
}
});
</script>
<template>
<div class="verify-page min-h-screen flex items-center justify-center py-12">
<div class="container-custom">
<div class="max-w-2xl mx-auto text-center">
<!-- Big pulsing text -->
<div class="relative mb-8">
<div
class="text-9xl font-display font-bold text-transparent bg-clip-text animate-pulse-slow"
:class="{
'bg-gradient-to-r from-green-500 to-green-700': success,
'bg-gradient-to-r from-red-500 to-red-700': error,
'bg-gradient-to-r from-primary-500 to-primary-700': loading,
}"
>
{{ success ? "SUCCESS" : error ? "ERROR" : "VERIFY" }}
</div>
<div class="absolute inset-0 flex items-center justify-center">
<div
class="w-64 h-64 rounded-full blur-3xl animate-pulse"
:class="{
'bg-green-500/10': success,
'bg-red-500/10': error,
'bg-primary-500/10': loading,
}"
></div>
</div>
</div>
<!-- Message -->
<p class="text-lg text-gray-400 mb-2 max-w-md mx-auto">
{{ message }}
</p>
<!-- Countdown -->
<p v-if="success" class="text-gray-400 mb-8">
Redirecting in {{ countdown }} second{{ countdown > 1 ? "s" : "" }}...
</p>
<p v-else class="mb-8"></p>
</div>
</div>
</div>
</template>
<style scoped>
.verify-page {
background: linear-gradient(
135deg,
rgba(15, 25, 35, 0.95) 0%,
rgba(21, 29, 40, 0.98) 50%,
rgba(26, 35, 50, 0.95) 100%
);
}
</style>

View File

@@ -720,7 +720,7 @@
<form @submit.prevent="disable2FA">
<div class="mb-4">
<label class="block text-sm font-medium text-text-secondary mb-2">
Enter your 6-digit 2FA code or recovery code
Please enter your recovery code
</label>
<input
v-model="disable2FAForm.code"

View File

@@ -601,8 +601,24 @@ const start = async () => {
.updateAllPrices()
.then((result) => {
console.log("✅ Initial price update completed successfully");
console.log(` CS2: ${result.cs2.updated || 0} items updated`);
console.log(` Rust: ${result.rust.updated || 0} items updated`);
console.log(" 📊 MarketPrice Reference Database:");
console.log(
` CS2: ${
result.marketPrices.cs2.updated || 0
} prices updated, ${result.marketPrices.cs2.inserted || 0} new`
);
console.log(
` Rust: ${
result.marketPrices.rust.updated || 0
} prices updated, ${result.marketPrices.rust.inserted || 0} new`
);
console.log(" 📦 Marketplace Items:");
console.log(
` CS2: ${result.itemPrices.cs2.updated || 0} items updated`
);
console.log(
` Rust: ${result.itemPrices.rust.updated || 0} items updated`
);
})
.catch((error) => {
console.error("❌ Initial price update failed:", error.message);

View File

@@ -105,25 +105,41 @@ export const authenticate = async (request, reply) => {
// Check if user is banned
if (user.ban && user.ban.banned) {
if (user.ban.expires && new Date(user.ban.expires) > new Date()) {
// Check if ban has expired
if (user.ban.expires && new Date(user.ban.expires) <= new Date()) {
// Ban expired, clear it
user.ban.banned = false;
user.ban.reason = null;
user.ban.expires = null;
await user.save();
} else {
// User is currently banned
// Allow access to /api/auth/me so frontend can get ban info and redirect
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
if (user.ban.expires) {
return reply.status(403).send({
error: "Forbidden",
message: "Your account is banned",
reason: user.ban.reason,
expires: user.ban.expires,
});
} else if (!user.ban.expires) {
} else {
return reply.status(403).send({
error: "Forbidden",
message: "Your account is permanently banned",
reason: user.ban.reason,
});
} else {
// Ban expired, clear it
user.ban.banned = false;
user.ban.reason = null;
user.ban.expires = null;
await user.save();
}
}
// If it's /api/auth/me, continue and attach user with ban info
}
}

View File

@@ -95,6 +95,22 @@ const SiteConfigSchema = new mongoose.Schema(
priceUpdateInterval: { type: Number, default: 3600000 }, // 1 hour in ms
},
// Instant sell / buyback settings (site buying items from users)
instantSell: {
enabled: { type: Boolean, default: true },
payoutRate: { type: Number, default: 0.6 }, // 60% of market price
minItemValue: { type: Number, default: 0.1 }, // Min $0.10
maxItemValue: { type: Number, default: 10000 }, // Max $10,000
cs2: {
enabled: { type: Boolean, default: true },
payoutRate: { type: Number, default: 0.6 }, // Game-specific override
},
rust: {
enabled: { type: Boolean, default: true },
payoutRate: { type: Number, default: 0.6 }, // Game-specific override
},
},
// Features toggles
features: {
twoFactorAuth: { type: Boolean, default: true },

View File

@@ -4,6 +4,7 @@ import Transaction from "../models/Transaction.js";
import SiteConfig from "../models/SiteConfig.js";
import PromoUsage from "../models/PromoUsage.js";
import { v4 as uuidv4 } from "uuid";
import wsManager from "../utils/websocket.js";
/**
* Admin management routes for user administration and site configuration
@@ -414,6 +415,32 @@ export default async function adminManagementRoutes(fastify, options) {
}`
);
// Send WebSocket notification to the user if they're connected
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(),
});
console.log(`📡 Sent ban notification to user ${user.username}`);
} else {
wsManager.sendToUser(user.steamId, {
type: "account_unbanned",
data: {
banned: false,
},
timestamp: Date.now(),
});
console.log(`📡 Sent unban notification to user ${user.username}`);
}
}
return reply.send({
success: true,
message: banned
@@ -786,6 +813,98 @@ export default async function adminManagementRoutes(fastify, options) {
}
);
// PATCH /admin/config/instantsell - Update instant sell settings
fastify.patch(
"/config/instantsell",
{
preHandler: [authenticate, isAdmin],
schema: {
body: {
type: "object",
properties: {
enabled: { type: "boolean" },
payoutRate: { type: "number", minimum: 0, maximum: 1 },
minItemValue: { type: "number", minimum: 0 },
maxItemValue: { type: "number", minimum: 0 },
cs2: {
type: "object",
properties: {
enabled: { type: "boolean" },
payoutRate: { type: "number", minimum: 0, maximum: 1 },
},
},
rust: {
type: "object",
properties: {
enabled: { type: "boolean" },
payoutRate: { type: "number", minimum: 0, maximum: 1 },
},
},
},
},
},
},
async (request, reply) => {
try {
const config = await SiteConfig.getConfig();
// Update top-level instant sell settings
["enabled", "payoutRate", "minItemValue", "maxItemValue"].forEach(
(key) => {
if (request.body[key] !== undefined) {
config.instantSell[key] = request.body[key];
}
}
);
// Update CS2 settings
if (request.body.cs2) {
Object.keys(request.body.cs2).forEach((key) => {
if (request.body.cs2[key] !== undefined) {
config.instantSell.cs2[key] = request.body.cs2[key];
}
});
}
// Update Rust settings
if (request.body.rust) {
Object.keys(request.body.rust).forEach((key) => {
if (request.body.rust[key] !== undefined) {
config.instantSell.rust[key] = request.body.rust[key];
}
});
}
config.lastUpdatedBy = request.user.username;
config.lastUpdatedAt = new Date();
await config.save();
console.log(
`⚙️ Admin ${request.user.username} updated instant sell settings:`,
{
payoutRate: config.instantSell.payoutRate,
cs2PayoutRate: config.instantSell.cs2?.payoutRate,
rustPayoutRate: config.instantSell.rust?.payoutRate,
}
);
return reply.send({
success: true,
message: "Instant sell settings updated",
instantSell: config.instantSell,
});
} catch (error) {
console.error("❌ Failed to update instant sell settings:", error);
return reply.status(500).send({
success: false,
message: "Failed to update instant sell settings",
error: error.message,
});
}
}
);
// ============================================
// ANNOUNCEMENTS
// ============================================

View File

@@ -1,6 +1,7 @@
import { authenticate } from "../middleware/auth.js";
import pricingService from "../services/pricing.js";
import Item from "../models/Item.js";
import MarketPrice from "../models/MarketPrice.js";
import Transaction from "../models/Transaction.js";
import User from "../models/User.js";
@@ -19,17 +20,13 @@ export default async function adminRoutes(fastify, options) {
});
}
// Check if user is admin (you can customize this check)
// For now, checking if user has admin role or specific steamId
const adminSteamIds = process.env.ADMIN_STEAM_IDS?.split(",") || [];
if (
!request.user.isAdmin &&
!adminSteamIds.includes(request.user.steamId)
) {
// Check if user has admin staff level (3 or higher)
if (!request.user.staffLevel || request.user.staffLevel < 3) {
return reply.status(403).send({
success: false,
message: "Admin access required",
requiredLevel: 3,
currentLevel: request.user.staffLevel || 0,
});
}
};
@@ -1115,4 +1112,242 @@ export default async function adminRoutes(fastify, options) {
}
}
);
// ============================================
// MARKETPRICE MANAGEMENT
// ============================================
// GET /admin/marketprices - Get paginated list of MarketPrice items
fastify.get(
"/marketprices",
{
preHandler: [authenticate, isAdmin],
schema: {
querystring: {
type: "object",
properties: {
page: { type: "integer", minimum: 1, default: 1 },
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
game: { type: "string", enum: ["cs2", "rust", ""] },
search: { type: "string" },
sort: { type: "string", default: "name-asc" },
},
},
},
},
async (request, reply) => {
try {
const {
page = 1,
limit = 50,
game = "",
search = "",
sort = "name-asc",
} = request.query;
// Build query
const query = {};
if (game) {
query.game = game;
}
if (search) {
query.$or = [
{ name: { $regex: search, $options: "i" } },
{ marketHashName: { $regex: search, $options: "i" } },
];
}
// Build sort
let sortObj = {};
switch (sort) {
case "name-asc":
sortObj = { name: 1 };
break;
case "name-desc":
sortObj = { name: -1 };
break;
case "price-asc":
sortObj = { price: 1 };
break;
case "price-desc":
sortObj = { price: -1 };
break;
case "updated-desc":
sortObj = { lastUpdated: -1 };
break;
default:
sortObj = { name: 1 };
}
// Get total count
const total = await MarketPrice.countDocuments(query);
// Get items
const items = await MarketPrice.find(query)
.sort(sortObj)
.limit(limit)
.skip((page - 1) * limit)
.lean();
return reply.send({
success: true,
items,
page,
limit,
total,
pages: Math.ceil(total / limit),
});
} catch (error) {
console.error("❌ Failed to get market prices:", error);
return reply.status(500).send({
success: false,
message: "Failed to get market prices",
error: error.message,
});
}
}
);
// GET /admin/marketprices/stats - Get MarketPrice statistics
fastify.get(
"/marketprices/stats",
{
preHandler: [authenticate, isAdmin],
},
async (request, reply) => {
try {
const total = await MarketPrice.countDocuments();
const cs2 = await MarketPrice.countDocuments({ game: "cs2" });
const rust = await MarketPrice.countDocuments({ game: "rust" });
// Get most recent update
const lastItem = await MarketPrice.findOne()
.sort({ lastUpdated: -1 })
.select("lastUpdated")
.lean();
return reply.send({
success: true,
stats: {
total,
cs2,
rust,
lastUpdated: lastItem?.lastUpdated || null,
},
});
} catch (error) {
console.error("❌ Failed to get market price stats:", error);
return reply.status(500).send({
success: false,
message: "Failed to get stats",
error: error.message,
});
}
}
);
// PATCH /admin/marketprices/:id - Update a MarketPrice item
fastify.patch(
"/marketprices/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
properties: {
price: { type: "number", minimum: 0 },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const { price } = request.body;
const item = await MarketPrice.findById(id);
if (!item) {
return reply.status(404).send({
success: false,
message: "Item not found",
});
}
item.price = price;
item.priceType = "manual"; // Mark as manually updated
item.lastUpdated = new Date();
await item.save();
console.log(
`⚙️ Admin ${request.user.username} updated price for ${item.name}: $${price}`
);
return reply.send({
success: true,
message: "Price updated successfully",
item,
});
} catch (error) {
console.error("❌ Failed to update market price:", error);
return reply.status(500).send({
success: false,
message: "Failed to update price",
error: error.message,
});
}
}
);
// DELETE /admin/marketprices/:id - Delete a MarketPrice item
fastify.delete(
"/marketprices/:id",
{
preHandler: [authenticate, isAdmin],
schema: {
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
},
},
async (request, reply) => {
try {
const { id } = request.params;
const item = await MarketPrice.findByIdAndDelete(id);
if (!item) {
return reply.status(404).send({
success: false,
message: "Item not found",
});
}
console.log(
`⚙️ Admin ${request.user.username} deleted market price for ${item.name}`
);
return reply.send({
success: true,
message: "Item deleted successfully",
});
} catch (error) {
console.error("❌ Failed to delete market price:", error);
return reply.status(500).send({
success: false,
message: "Failed to delete item",
error: error.message,
});
}
}
);
}

View File

@@ -3,6 +3,7 @@ import { authenticate } from "../middleware/auth.js";
import Item from "../models/Item.js";
import Trade from "../models/Trade.js";
import Transaction from "../models/Transaction.js";
import SiteConfig from "../models/SiteConfig.js";
import { config } from "../config/index.js";
import pricingService from "../services/pricing.js";
import marketPriceService from "../services/marketPrice.js";
@@ -184,6 +185,23 @@ export default async function inventoryRoutes(fastify, options) {
// Enrich items with market prices (fast database lookup)
console.log(`💰 Adding market prices...`);
// Get site config for payout rate
const siteConfig = await SiteConfig.getConfig();
let payoutRate = siteConfig.instantSell.payoutRate || 0.6;
// Check for game-specific payout rate
if (game === "cs2" && siteConfig.instantSell.cs2?.payoutRate) {
payoutRate = siteConfig.instantSell.cs2.payoutRate;
} else if (game === "rust" && siteConfig.instantSell.rust?.payoutRate) {
payoutRate = siteConfig.instantSell.rust.payoutRate;
}
console.log(
`💵 Instant sell payout rate: ${(payoutRate * 100).toFixed(
0
)}% of market price`
);
// Get all item names for batch lookup
const itemNames = items.map((item) => item.name);
console.log(`📋 Looking up prices for ${itemNames.length} items`);
@@ -196,12 +214,19 @@ export default async function inventoryRoutes(fastify, options) {
`💰 Found prices for ${foundPrices}/${itemNames.length} items`
);
// Add prices to items
const enrichedItems = items.map((item) => ({
// Add prices to items (applying payout rate for instant sell)
const enrichedItems = items.map((item) => {
const marketPrice = priceMap[item.name] || null;
return {
...item,
marketPrice: priceMap[item.name] || null,
hasPriceData: !!priceMap[item.name],
}));
marketPrice: marketPrice
? parseFloat((marketPrice * payoutRate).toFixed(2))
: null,
fullMarketPrice: marketPrice, // Keep original for reference
payoutRate: payoutRate,
hasPriceData: !!marketPrice,
};
});
// Log items without prices
const itemsWithoutPrices = enrichedItems.filter(

View File

@@ -1,5 +1,6 @@
import axios from "axios";
import Item from "../models/Item.js";
import MarketPrice from "../models/MarketPrice.js";
/**
* Pricing Service
@@ -275,15 +276,177 @@ class PricingService {
}
/**
* Update prices for all games
* Update MarketPrice reference database for a specific game
* Fetches all items from Steam market and updates the reference database
* @param {string} game - Game identifier ('cs2' or 'rust')
* @returns {Promise<Object>} - Update statistics
*/
async updateMarketPriceDatabase(game) {
console.log(
`\n🔄 Updating MarketPrice reference database for ${game.toUpperCase()}...`
);
try {
const appId = this.appIds[game];
if (!appId) {
throw new Error(`Invalid game: ${game}`);
}
if (!this.apiKey) {
throw new Error("Steam API key not configured");
}
// Fetch all market items from Steam API
console.log(`📡 Fetching market data from Steam API...`);
const response = await axios.get(
`${this.baseUrl}/market/items/${appId}`,
{
params: { api_key: this.apiKey },
timeout: 60000,
}
);
if (!response.data || !response.data.data) {
throw new Error("No data returned from Steam API");
}
const items = response.data.data;
const itemCount = Object.keys(items).length;
console.log(`✅ Received ${itemCount} items from API`);
let inserted = 0;
let updated = 0;
let skipped = 0;
let errors = 0;
// Process items in batches
const bulkOps = [];
for (const item of Object.values(items)) {
try {
// Get the best available price
const price =
item.prices?.safe ||
item.prices?.median ||
item.prices?.mean ||
item.prices?.avg ||
item.prices?.latest;
if (!price || price <= 0) {
skipped++;
continue;
}
const marketHashName = item.market_hash_name || item.market_name;
const marketName = item.market_name || item.market_hash_name;
if (!marketHashName || !marketName) {
skipped++;
continue;
}
// Determine which price type was used
let priceType = "safe";
if (item.prices?.safe) priceType = "safe";
else if (item.prices?.median) priceType = "median";
else if (item.prices?.mean) priceType = "mean";
else if (item.prices?.avg) priceType = "avg";
else if (item.prices?.latest) priceType = "latest";
bulkOps.push({
updateOne: {
filter: { marketHashName: marketHashName },
update: {
$set: {
name: marketName,
game: game,
appId: appId,
marketHashName: marketHashName,
price: price,
priceType: priceType,
image: item.image || null,
borderColor: item.border_color || null,
nameId: item.nameID || null,
lastUpdated: new Date(),
},
},
upsert: true,
},
});
// Execute in batches of 1000
if (bulkOps.length >= 1000) {
const result = await MarketPrice.bulkWrite(bulkOps);
inserted += result.upsertedCount;
updated += result.modifiedCount;
console.log(
` 📦 Batch: ${inserted} inserted, ${updated} updated`
);
bulkOps.length = 0;
}
} catch (err) {
errors++;
}
}
// Execute remaining items
if (bulkOps.length > 0) {
const result = await MarketPrice.bulkWrite(bulkOps);
inserted += result.upsertedCount;
updated += result.modifiedCount;
}
console.log(`✅ MarketPrice update complete for ${game.toUpperCase()}:`);
console.log(` 📥 Inserted: ${inserted}`);
console.log(` 🔄 Updated: ${updated}`);
console.log(` ⏭️ Skipped: ${skipped}`);
if (errors > 0) {
console.log(` ❌ Errors: ${errors}`);
}
return {
success: true,
game,
total: itemCount,
inserted,
updated,
skipped,
errors,
};
} catch (error) {
console.error(
`❌ Error updating MarketPrice for ${game}:`,
error.message
);
return {
success: false,
game,
error: error.message,
total: 0,
inserted: 0,
updated: 0,
skipped: 0,
errors: 1,
};
}
}
/**
* Update prices for all games (both Item and MarketPrice databases)
* @returns {Promise<Object>} - Combined update statistics
*/
async updateAllPrices() {
console.log("🔄 Starting price update for all games...");
const results = {
marketPrices: {
cs2: await this.updateMarketPriceDatabase("cs2"),
rust: await this.updateMarketPriceDatabase("rust"),
},
itemPrices: {
cs2: await this.updateDatabasePrices("cs2"),
rust: await this.updateDatabasePrices("rust"),
},
timestamp: new Date(),
};