Compare commits
19 Commits
bfa028c630
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ae3fbe0261 | |||
| 5da4c0f34f | |||
| d736e9c69f | |||
| 15e3eb316a | |||
| b05594fe3d | |||
| 0f7f1ae5dd | |||
| 87b290032c | |||
| af5c32561a | |||
| cbf50b0641 | |||
| 5848323140 | |||
| 2aff879291 | |||
| d794c5ad48 | |||
| 5846541329 | |||
| aca0aca310 | |||
| 02d9727a72 | |||
| 7a32454b83 | |||
| b686acee8f | |||
| 1f62e148e5 | |||
| ac72c6ad27 |
9
.env
9
.env
@@ -23,6 +23,7 @@ STEAM_RETURN_URL=https://api.turbotrades.dev/auth/steam/return
|
|||||||
|
|
||||||
#Steam apis key - Loading the inventory
|
#Steam apis key - Loading the inventory
|
||||||
STEAM_APIS_KEY=DONTABUSEORPEPZWILLNAGASAKI
|
STEAM_APIS_KEY=DONTABUSEORPEPZWILLNAGASAKI
|
||||||
|
ENABLE_PRICE_UPDATES="true"
|
||||||
|
|
||||||
# Cookie Settings
|
# Cookie Settings
|
||||||
COOKIE_DOMAIN=.turbotrades.dev
|
COOKIE_DOMAIN=.turbotrades.dev
|
||||||
@@ -41,11 +42,11 @@ RATE_LIMIT_TIMEWINDOW=60000
|
|||||||
BYPASS_BOT_REQUIREMENT=true
|
BYPASS_BOT_REQUIREMENT=true
|
||||||
|
|
||||||
# Email Configuration (for future implementation)
|
# Email Configuration (for future implementation)
|
||||||
SMTP_HOST=smtp.example.com
|
SMTP_HOST=mail.privateemail.com
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_USER=your-email@example.com
|
SMTP_USER=spam@turbotrades.gg
|
||||||
SMTP_PASS=your-email-password
|
SMTP_PASS=20yBBj!0
|
||||||
EMAIL_FROM=noreply@turbotrades.com
|
EMAIL_FROM=noreply@turbotrades.gg
|
||||||
|
|
||||||
# WebSocket
|
# WebSocket
|
||||||
WS_PING_INTERVAL=30000
|
WS_PING_INTERVAL=30000
|
||||||
|
|||||||
277
BAN_NOTIFICATION_FIX.md
Normal file
277
BAN_NOTIFICATION_FIX.md
Normal 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
288
FINAL_DEPLOY_STEPS.md
Normal 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
395
PRICING_SYSTEM_COMPLETE.md
Normal 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
176
deploy-ban-fix.sh
Normal 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}"
|
||||||
@@ -496,6 +496,80 @@
|
|||||||
<p class="form-help">How often to auto-update market prices</p>
|
<p class="form-help">How often to auto-update market prices</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="form-actions-centered">
|
<div class="form-actions-centered">
|
||||||
@@ -970,6 +1044,22 @@ const promotionForm = ref({
|
|||||||
code: "",
|
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
|
// Methods
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -1016,6 +1106,25 @@ const loadConfig = async () => {
|
|||||||
commission: config.value.market.commission * 100,
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to load config:", error);
|
console.error("Failed to load config:", error);
|
||||||
@@ -1060,25 +1169,46 @@ const saveAllSettings = async () => {
|
|||||||
commission: marketForm.value.commission / 100, // Convert percentage to decimal
|
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("💾 Saving all settings...");
|
||||||
console.log("Trading data:", tradingData);
|
console.log("Trading data:", tradingData);
|
||||||
console.log("Market data:", marketData);
|
console.log("Market data:", marketData);
|
||||||
|
console.log("Instant Sell data:", instantSellData);
|
||||||
|
|
||||||
// Save both in parallel
|
// Save all in parallel
|
||||||
const [tradingResponse, marketResponse] = await Promise.all([
|
const [tradingResponse, marketResponse, instantSellResponse] =
|
||||||
axios.patch("/api/admin/config/trading", tradingData),
|
await Promise.all([
|
||||||
axios.patch("/api/admin/config/market", marketData),
|
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) {
|
if (
|
||||||
toast.success("✅ All settings saved successfully!");
|
tradingResponse.data.success &&
|
||||||
await loadConfig();
|
marketResponse.data.success &&
|
||||||
|
instantSellResponse.data.success
|
||||||
|
) {
|
||||||
|
toast.success("Settings saved successfully");
|
||||||
|
await loadConfig(); // Reload to get updated values
|
||||||
} else {
|
} else {
|
||||||
throw new Error("One or more settings failed to save");
|
toast.error("Failed to save some settings");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to save settings:", error);
|
console.error("Failed to save settings:", error);
|
||||||
console.error("Error response:", error.response?.data);
|
|
||||||
toast.error(error.response?.data?.message || "Failed to save settings");
|
toast.error(error.response?.data?.message || "Failed to save settings");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
@@ -1885,7 +2015,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin-bottom: 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) {
|
@media (max-width: 1024px) {
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
523
frontend/src/components/AdminItemsPanel.vue
Normal file
523
frontend/src/components/AdminItemsPanel.vue
Normal 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>
|
||||||
@@ -104,6 +104,12 @@ const routes = [
|
|||||||
component: () => import("@/views/MaintenancePage.vue"),
|
component: () => import("@/views/MaintenancePage.vue"),
|
||||||
meta: { title: "Maintenance Mode" },
|
meta: { title: "Maintenance Mode" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/verify-email/:token",
|
||||||
|
name: "MailVerify",
|
||||||
|
component: () => import("@/views/MailVerify.vue"),
|
||||||
|
meta: { title: "Verify Email" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/banned",
|
path: "/banned",
|
||||||
name: "Banned",
|
name: "Banned",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
console.log("🔵 fetchUser called - fetching user from /api/auth/me");
|
console.log("🔵 fetchUser called - fetching user from /api/auth/me");
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/auth/me", {
|
const response = await axios.get("/api/auth/me", {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
"/auth/logout",
|
"/api/auth/logout",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@@ -111,7 +111,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
const refreshToken = async () => {
|
const refreshToken = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.post(
|
await axios.post(
|
||||||
"/auth/refresh",
|
"/api/auth/refresh",
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
@@ -129,7 +129,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.patch(
|
const response = await axios.patch(
|
||||||
"/user/trade-url",
|
"/api/user/trade-url",
|
||||||
{ tradeUrl },
|
{ tradeUrl },
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
);
|
);
|
||||||
@@ -155,7 +155,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.patch(
|
const response = await axios.patch(
|
||||||
"/user/email",
|
"/api/user/email",
|
||||||
{ email },
|
{ email },
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
);
|
);
|
||||||
@@ -178,7 +178,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
const verifyEmail = async (token) => {
|
const verifyEmail = async (token) => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/user/verify-email/${token}`, {
|
const response = await axios.get(`/api/user/verify-email/${token}`, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
|
|
||||||
const getUserStats = async () => {
|
const getUserStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/user/stats", {
|
const response = await axios.get("/api/user/stats", {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
|
|
||||||
const getBalance = async () => {
|
const getBalance = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get("/user/balance", {
|
const response = await axios.get("/api/user/balance", {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,28 @@ export const useWebSocketStore = defineStore("websocket", () => {
|
|||||||
authStore.fetchUser();
|
authStore.fetchUser();
|
||||||
break;
|
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":
|
case "pong":
|
||||||
// Heartbeat response
|
// Heartbeat response
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const getApiBaseUrl = () => {
|
|||||||
return import.meta.env.VITE_API_URL;
|
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) {
|
if (import.meta.env.PROD) {
|
||||||
const currentHost = window.location.hostname;
|
const currentHost = window.location.hostname;
|
||||||
const protocol = window.location.protocol;
|
const protocol = window.location.protocol;
|
||||||
@@ -20,13 +20,13 @@ const getApiBaseUrl = () => {
|
|||||||
currentHost === "turbotrades.dev" ||
|
currentHost === "turbotrades.dev" ||
|
||||||
currentHost === "www.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);
|
console.log("🔵 Production API baseURL:", baseUrl);
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other domains, try api subdomain
|
// 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);
|
console.log("🔵 Production API baseURL (custom domain):", baseUrl);
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
@@ -37,8 +37,16 @@ const getApiBaseUrl = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Create axios instance
|
// 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({
|
const axiosInstance = axios.create({
|
||||||
baseURL: getApiBaseUrl(),
|
baseURL: baseURL,
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
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
|
// Request interceptor
|
||||||
axiosInstance.interceptors.request.use(
|
axiosInstance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
// You can add auth token to headers here if needed
|
// Log every request for debugging
|
||||||
// const token = localStorage.getItem('token')
|
const fullUrl = `${config.baseURL}${config.url}`;
|
||||||
// if (token) {
|
console.log(`📤 Axios Request: ${config.method.toUpperCase()} ${fullUrl}`);
|
||||||
// config.headers.Authorization = `Bearer ${token}`
|
|
||||||
// }
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
console.error("❌ Axios Request Error:", error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -477,7 +477,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Items Tab -->
|
<!-- 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 -->
|
<!-- Game Filter -->
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -753,6 +758,7 @@ import axios from "../utils/axios";
|
|||||||
import AdminUsersPanel from "../components/AdminUsersPanel.vue";
|
import AdminUsersPanel from "../components/AdminUsersPanel.vue";
|
||||||
import AdminConfigPanel from "../components/AdminConfigPanel.vue";
|
import AdminConfigPanel from "../components/AdminConfigPanel.vue";
|
||||||
import AdminDebugPanel from "../components/AdminDebugPanel.vue";
|
import AdminDebugPanel from "../components/AdminDebugPanel.vue";
|
||||||
|
import AdminItemsPanel from "../components/AdminItemsPanel.vue";
|
||||||
import {
|
import {
|
||||||
Shield,
|
Shield,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
|||||||
@@ -9,62 +9,35 @@
|
|||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h1 class="banned-title">Account Suspended</h1>
|
<h1 class="banned-title">Account Suspended</h1>
|
||||||
|
|
||||||
<!-- Message -->
|
<!-- Ban Details and Appeal Section -->
|
||||||
<p class="banned-message">
|
<div class="ban-details-container">
|
||||||
Your account has been suspended due to a violation of our Terms of
|
<div v-if="banInfo" class="ban-details">
|
||||||
Service.
|
<div class="detail-item">
|
||||||
</p>
|
<span class="detail-label">Reason:</span>
|
||||||
|
<span class="detail-value">{{
|
||||||
|
banInfo.reason || "Violation of Terms of Service"
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ban Details -->
|
<div v-if="banInfo.bannedUntil" class="detail-item">
|
||||||
<div v-if="banInfo" class="ban-details">
|
<span class="detail-label">Ban expires:</span>
|
||||||
<div class="detail-item">
|
<span class="detail-value">{{
|
||||||
<span class="detail-label">Reason:</span>
|
formatDate(banInfo.bannedUntil)
|
||||||
<span class="detail-value">{{
|
}}</span>
|
||||||
banInfo.reason || "Violation of Terms of Service"
|
</div>
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="banInfo.bannedAt" class="detail-item">
|
<div class="appeal-section">
|
||||||
<span class="detail-label">Banned on:</span>
|
<p class="appeal-text">
|
||||||
<span class="detail-value">{{ formatDate(banInfo.bannedAt) }}</span>
|
If you believe this ban was made in error, you can submit an appeal.
|
||||||
</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>
|
</p>
|
||||||
|
<a href="/support" class="appeal-btn">
|
||||||
|
<Mail :size="20" />
|
||||||
|
<span>Contact Support</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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.
|
|
||||||
</p>
|
|
||||||
<a href="/support" class="appeal-btn">
|
|
||||||
<Mail :size="20" />
|
|
||||||
<span>Contact Support</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Logout Button -->
|
<!-- Logout Button -->
|
||||||
<button @click="handleLogout" class="logout-btn">
|
<button @click="handleLogout" class="logout-btn">
|
||||||
<LogOut :size="20" />
|
<LogOut :size="20" />
|
||||||
@@ -134,10 +107,7 @@ const banInfo = computed(() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
reason: authStore.user.ban?.reason,
|
reason: authStore.user.ban?.reason,
|
||||||
bannedAt: authStore.user.ban?.bannedAt,
|
bannedUntil: authStore.user.ban?.expires,
|
||||||
bannedUntil: authStore.user.ban?.bannedUntil,
|
|
||||||
isPermanent:
|
|
||||||
authStore.user.ban?.permanent || !authStore.user.ban?.bannedUntil,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,15 +263,6 @@ onMounted(() => {
|
|||||||
line-height: 1.6;
|
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 {
|
.detail-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -329,14 +290,6 @@ onMounted(() => {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.permanent-ban {
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: #ef4444;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-box {
|
.info-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -372,23 +325,40 @@ onMounted(() => {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appeal-section {
|
.ban-details-container {
|
||||||
margin: 2rem 0;
|
background: rgba(239, 68, 68, 0.1);
|
||||||
padding: 1.5rem;
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 1rem;
|
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 {
|
.appeal-text {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appeal-btn {
|
.appeal-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.875rem 2rem;
|
padding: 0.875rem 2rem;
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||||
@@ -400,6 +370,8 @@ onMounted(() => {
|
|||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||||
border: 1px solid rgba(59, 130, 246, 0.5);
|
border: 1px solid rgba(59, 130, 246, 0.5);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appeal-btn:hover {
|
.appeal-btn:hover {
|
||||||
@@ -524,8 +496,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.appeal-btn {
|
.appeal-btn {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
113
frontend/src/views/MailVerify.vue
Normal file
113
frontend/src/views/MailVerify.vue
Normal 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>
|
||||||
@@ -720,7 +720,7 @@
|
|||||||
<form @submit.prevent="disable2FA">
|
<form @submit.prevent="disable2FA">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-text-secondary mb-2">
|
<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>
|
</label>
|
||||||
<input
|
<input
|
||||||
v-model="disable2FAForm.code"
|
v-model="disable2FAForm.code"
|
||||||
|
|||||||
20
index.js
20
index.js
@@ -601,8 +601,24 @@ const start = async () => {
|
|||||||
.updateAllPrices()
|
.updateAllPrices()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log("✅ Initial price update completed successfully");
|
console.log("✅ Initial price update completed successfully");
|
||||||
console.log(` CS2: ${result.cs2.updated || 0} items updated`);
|
console.log(" 📊 MarketPrice Reference Database:");
|
||||||
console.log(` Rust: ${result.rust.updated || 0} items updated`);
|
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) => {
|
.catch((error) => {
|
||||||
console.error("❌ Initial price update failed:", error.message);
|
console.error("❌ Initial price update failed:", error.message);
|
||||||
|
|||||||
@@ -105,25 +105,41 @@ export const authenticate = async (request, reply) => {
|
|||||||
|
|
||||||
// Check if user is banned
|
// Check if user is banned
|
||||||
if (user.ban && user.ban.banned) {
|
if (user.ban && user.ban.banned) {
|
||||||
if (user.ban.expires && new Date(user.ban.expires) > new Date()) {
|
// Check if ban has expired
|
||||||
return reply.status(403).send({
|
if (user.ban.expires && new Date(user.ban.expires) <= new Date()) {
|
||||||
error: "Forbidden",
|
|
||||||
message: "Your account is banned",
|
|
||||||
reason: user.ban.reason,
|
|
||||||
expires: user.ban.expires,
|
|
||||||
});
|
|
||||||
} else if (!user.ban.expires) {
|
|
||||||
return reply.status(403).send({
|
|
||||||
error: "Forbidden",
|
|
||||||
message: "Your account is permanently banned",
|
|
||||||
reason: user.ban.reason,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Ban expired, clear it
|
// Ban expired, clear it
|
||||||
user.ban.banned = false;
|
user.ban.banned = false;
|
||||||
user.ban.reason = null;
|
user.ban.reason = null;
|
||||||
user.ban.expires = null;
|
user.ban.expires = null;
|
||||||
await user.save();
|
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 {
|
||||||
|
return reply.status(403).send({
|
||||||
|
error: "Forbidden",
|
||||||
|
message: "Your account is permanently banned",
|
||||||
|
reason: user.ban.reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If it's /api/auth/me, continue and attach user with ban info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,22 @@ const SiteConfigSchema = new mongoose.Schema(
|
|||||||
priceUpdateInterval: { type: Number, default: 3600000 }, // 1 hour in ms
|
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 toggles
|
||||||
features: {
|
features: {
|
||||||
twoFactorAuth: { type: Boolean, default: true },
|
twoFactorAuth: { type: Boolean, default: true },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Transaction from "../models/Transaction.js";
|
|||||||
import SiteConfig from "../models/SiteConfig.js";
|
import SiteConfig from "../models/SiteConfig.js";
|
||||||
import PromoUsage from "../models/PromoUsage.js";
|
import PromoUsage from "../models/PromoUsage.js";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import wsManager from "../utils/websocket.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin management routes for user administration and site configuration
|
* 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({
|
return reply.send({
|
||||||
success: true,
|
success: true,
|
||||||
message: banned
|
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
|
// ANNOUNCEMENTS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
251
routes/admin.js
251
routes/admin.js
@@ -1,6 +1,7 @@
|
|||||||
import { authenticate } from "../middleware/auth.js";
|
import { authenticate } from "../middleware/auth.js";
|
||||||
import pricingService from "../services/pricing.js";
|
import pricingService from "../services/pricing.js";
|
||||||
import Item from "../models/Item.js";
|
import Item from "../models/Item.js";
|
||||||
|
import MarketPrice from "../models/MarketPrice.js";
|
||||||
import Transaction from "../models/Transaction.js";
|
import Transaction from "../models/Transaction.js";
|
||||||
import User from "../models/User.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)
|
// Check if user has admin staff level (3 or higher)
|
||||||
// For now, checking if user has admin role or specific steamId
|
if (!request.user.staffLevel || request.user.staffLevel < 3) {
|
||||||
const adminSteamIds = process.env.ADMIN_STEAM_IDS?.split(",") || [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!request.user.isAdmin &&
|
|
||||||
!adminSteamIds.includes(request.user.steamId)
|
|
||||||
) {
|
|
||||||
return reply.status(403).send({
|
return reply.status(403).send({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Admin access required",
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { authenticate } from "../middleware/auth.js";
|
|||||||
import Item from "../models/Item.js";
|
import Item from "../models/Item.js";
|
||||||
import Trade from "../models/Trade.js";
|
import Trade from "../models/Trade.js";
|
||||||
import Transaction from "../models/Transaction.js";
|
import Transaction from "../models/Transaction.js";
|
||||||
|
import SiteConfig from "../models/SiteConfig.js";
|
||||||
import { config } from "../config/index.js";
|
import { config } from "../config/index.js";
|
||||||
import pricingService from "../services/pricing.js";
|
import pricingService from "../services/pricing.js";
|
||||||
import marketPriceService from "../services/marketPrice.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)
|
// Enrich items with market prices (fast database lookup)
|
||||||
console.log(`💰 Adding market prices...`);
|
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
|
// Get all item names for batch lookup
|
||||||
const itemNames = items.map((item) => item.name);
|
const itemNames = items.map((item) => item.name);
|
||||||
console.log(`📋 Looking up prices for ${itemNames.length} items`);
|
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`
|
`💰 Found prices for ${foundPrices}/${itemNames.length} items`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add prices to items
|
// Add prices to items (applying payout rate for instant sell)
|
||||||
const enrichedItems = items.map((item) => ({
|
const enrichedItems = items.map((item) => {
|
||||||
...item,
|
const marketPrice = priceMap[item.name] || null;
|
||||||
marketPrice: priceMap[item.name] || null,
|
return {
|
||||||
hasPriceData: !!priceMap[item.name],
|
...item,
|
||||||
}));
|
marketPrice: marketPrice
|
||||||
|
? parseFloat((marketPrice * payoutRate).toFixed(2))
|
||||||
|
: null,
|
||||||
|
fullMarketPrice: marketPrice, // Keep original for reference
|
||||||
|
payoutRate: payoutRate,
|
||||||
|
hasPriceData: !!marketPrice,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Log items without prices
|
// Log items without prices
|
||||||
const itemsWithoutPrices = enrichedItems.filter(
|
const itemsWithoutPrices = enrichedItems.filter(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Item from "../models/Item.js";
|
import Item from "../models/Item.js";
|
||||||
|
import MarketPrice from "../models/MarketPrice.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pricing Service
|
* 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
|
* @returns {Promise<Object>} - Combined update statistics
|
||||||
*/
|
*/
|
||||||
async updateAllPrices() {
|
async updateAllPrices() {
|
||||||
console.log("🔄 Starting price update for all games...");
|
console.log("🔄 Starting price update for all games...");
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
cs2: await this.updateDatabasePrices("cs2"),
|
marketPrices: {
|
||||||
rust: await this.updateDatabasePrices("rust"),
|
cs2: await this.updateMarketPriceDatabase("cs2"),
|
||||||
|
rust: await this.updateMarketPriceDatabase("rust"),
|
||||||
|
},
|
||||||
|
itemPrices: {
|
||||||
|
cs2: await this.updateDatabasePrices("cs2"),
|
||||||
|
rust: await this.updateDatabasePrices("rust"),
|
||||||
|
},
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user