first commit

This commit is contained in:
2026-01-10 04:57:43 +00:00
parent 16a76a2cd6
commit 232968de1e
131 changed files with 43262 additions and 0 deletions

260
frontend/src/stores/auth.js Normal file
View File

@@ -0,0 +1,260 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
import { useToast } from 'vue-toastification'
const toast = useToast()
export const useAuthStore = defineStore('auth', () => {
// State
const user = ref(null)
const isAuthenticated = ref(false)
const isLoading = ref(false)
const isInitialized = ref(false)
// Computed
const username = computed(() => user.value?.username || 'Guest')
const steamId = computed(() => user.value?.steamId || null)
const avatar = computed(() => user.value?.avatar || null)
const balance = computed(() => user.value?.balance || 0)
const staffLevel = computed(() => user.value?.staffLevel || 0)
const isStaff = computed(() => staffLevel.value > 0)
const isModerator = computed(() => staffLevel.value >= 2)
const isAdmin = computed(() => staffLevel.value >= 3)
const tradeUrl = computed(() => user.value?.tradeUrl || null)
const email = computed(() => user.value?.email?.address || null)
const emailVerified = computed(() => user.value?.email?.verified || false)
const isBanned = computed(() => user.value?.ban?.banned || false)
const banReason = computed(() => user.value?.ban?.reason || null)
const twoFactorEnabled = computed(() => user.value?.twoFactor?.enabled || false)
// Actions
const setUser = (userData) => {
user.value = userData
isAuthenticated.value = !!userData
}
const clearUser = () => {
user.value = null
isAuthenticated.value = false
}
const fetchUser = async () => {
if (isLoading.value) return
isLoading.value = true
try {
const response = await axios.get('/api/auth/me', {
withCredentials: true,
})
if (response.data.success && response.data.user) {
setUser(response.data.user)
return response.data.user
} else {
clearUser()
return null
}
} catch (error) {
console.error('Failed to fetch user:', error)
clearUser()
return null
} finally {
isLoading.value = false
isInitialized.value = true
}
}
const login = () => {
// Redirect to Steam login
window.location.href = '/api/auth/steam'
}
const logout = async () => {
isLoading.value = true
try {
await axios.post('/api/auth/logout', {}, {
withCredentials: true,
})
clearUser()
toast.success('Successfully logged out')
// Redirect to home page
if (window.location.pathname !== '/') {
window.location.href = '/'
}
} catch (error) {
console.error('Logout error:', error)
toast.error('Failed to logout')
} finally {
isLoading.value = false
}
}
const refreshToken = async () => {
try {
await axios.post('/api/auth/refresh', {}, {
withCredentials: true,
})
return true
} catch (error) {
console.error('Token refresh failed:', error)
clearUser()
return false
}
}
const updateTradeUrl = async (tradeUrl) => {
isLoading.value = true
try {
const response = await axios.patch('/api/user/trade-url',
{ tradeUrl },
{ withCredentials: true }
)
if (response.data.success) {
user.value.tradeUrl = tradeUrl
toast.success('Trade URL updated successfully')
return true
}
return false
} catch (error) {
console.error('Failed to update trade URL:', error)
toast.error(error.response?.data?.message || 'Failed to update trade URL')
return false
} finally {
isLoading.value = false
}
}
const updateEmail = async (email) => {
isLoading.value = true
try {
const response = await axios.patch('/api/user/email',
{ email },
{ withCredentials: true }
)
if (response.data.success) {
user.value.email = { address: email, verified: false }
toast.success('Email updated! Check your inbox for verification link')
return true
}
return false
} catch (error) {
console.error('Failed to update email:', error)
toast.error(error.response?.data?.message || 'Failed to update email')
return false
} finally {
isLoading.value = false
}
}
const verifyEmail = async (token) => {
isLoading.value = true
try {
const response = await axios.get(`/api/user/verify-email/${token}`, {
withCredentials: true
})
if (response.data.success) {
toast.success('Email verified successfully!')
await fetchUser() // Refresh user data
return true
}
return false
} catch (error) {
console.error('Failed to verify email:', error)
toast.error(error.response?.data?.message || 'Failed to verify email')
return false
} finally {
isLoading.value = false
}
}
const getUserStats = async () => {
try {
const response = await axios.get('/api/user/stats', {
withCredentials: true
})
if (response.data.success) {
return response.data.stats
}
return null
} catch (error) {
console.error('Failed to fetch user stats:', error)
return null
}
}
const getBalance = async () => {
try {
const response = await axios.get('/api/user/balance', {
withCredentials: true
})
if (response.data.success) {
user.value.balance = response.data.balance
return response.data.balance
}
return null
} catch (error) {
console.error('Failed to fetch balance:', error)
return null
}
}
const updateBalance = (newBalance) => {
if (user.value) {
user.value.balance = newBalance
}
}
// Initialize on store creation
const initialize = async () => {
if (!isInitialized.value) {
await fetchUser()
}
}
return {
// State
user,
isAuthenticated,
isLoading,
isInitialized,
// Computed
username,
steamId,
avatar,
balance,
staffLevel,
isStaff,
isModerator,
isAdmin,
tradeUrl,
email,
emailVerified,
isBanned,
banReason,
twoFactorEnabled,
// Actions
setUser,
clearUser,
fetchUser,
login,
logout,
refreshToken,
updateTradeUrl,
updateEmail,
verifyEmail,
getUserStats,
getBalance,
updateBalance,
initialize,
}
})

View File

@@ -0,0 +1,452 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import axios from 'axios'
import { useToast } from 'vue-toastification'
import { useWebSocketStore } from './websocket'
const toast = useToast()
export const useMarketStore = defineStore('market', () => {
// State
const items = ref([])
const featuredItems = ref([])
const recentSales = ref([])
const isLoading = ref(false)
const isLoadingMore = ref(false)
const currentPage = ref(1)
const totalPages = ref(1)
const totalItems = ref(0)
const itemsPerPage = ref(24)
// Filters
const filters = ref({
search: '',
game: null, // 'cs2', 'rust', null for all
minPrice: null,
maxPrice: null,
rarity: null,
wear: null,
category: null,
sortBy: 'price_asc', // price_asc, price_desc, name_asc, name_desc, date_new, date_old
statTrak: null,
souvenir: null,
})
// Categories
const categories = ref([
{ id: 'all', name: 'All Items', icon: 'Grid' },
{ id: 'rifles', name: 'Rifles', icon: 'Crosshair' },
{ id: 'pistols', name: 'Pistols', icon: 'Target' },
{ id: 'knives', name: 'Knives', icon: 'Sword' },
{ id: 'gloves', name: 'Gloves', icon: 'Hand' },
{ id: 'stickers', name: 'Stickers', icon: 'Sticker' },
{ id: 'cases', name: 'Cases', icon: 'Package' },
])
// Rarities
const rarities = ref([
{ id: 'common', name: 'Consumer Grade', color: '#b0c3d9' },
{ id: 'uncommon', name: 'Industrial Grade', color: '#5e98d9' },
{ id: 'rare', name: 'Mil-Spec', color: '#4b69ff' },
{ id: 'mythical', name: 'Restricted', color: '#8847ff' },
{ id: 'legendary', name: 'Classified', color: '#d32ce6' },
{ id: 'ancient', name: 'Covert', color: '#eb4b4b' },
{ id: 'exceedingly', name: 'Contraband', color: '#e4ae39' },
])
// Wear conditions
const wearConditions = ref([
{ id: 'fn', name: 'Factory New', abbr: 'FN' },
{ id: 'mw', name: 'Minimal Wear', abbr: 'MW' },
{ id: 'ft', name: 'Field-Tested', abbr: 'FT' },
{ id: 'ww', name: 'Well-Worn', abbr: 'WW' },
{ id: 'bs', name: 'Battle-Scarred', abbr: 'BS' },
])
// Computed
const filteredItems = computed(() => {
let result = [...items.value]
if (filters.value.search) {
const searchTerm = filters.value.search.toLowerCase()
result = result.filter(item =>
item.name.toLowerCase().includes(searchTerm) ||
item.description?.toLowerCase().includes(searchTerm)
)
}
if (filters.value.game) {
result = result.filter(item => item.game === filters.value.game)
}
if (filters.value.minPrice !== null) {
result = result.filter(item => item.price >= filters.value.minPrice)
}
if (filters.value.maxPrice !== null) {
result = result.filter(item => item.price <= filters.value.maxPrice)
}
if (filters.value.rarity) {
result = result.filter(item => item.rarity === filters.value.rarity)
}
if (filters.value.wear) {
result = result.filter(item => item.wear === filters.value.wear)
}
if (filters.value.category && filters.value.category !== 'all') {
result = result.filter(item => item.category === filters.value.category)
}
if (filters.value.statTrak !== null) {
result = result.filter(item => item.statTrak === filters.value.statTrak)
}
if (filters.value.souvenir !== null) {
result = result.filter(item => item.souvenir === filters.value.souvenir)
}
// Apply sorting
switch (filters.value.sortBy) {
case 'price_asc':
result.sort((a, b) => a.price - b.price)
break
case 'price_desc':
result.sort((a, b) => b.price - a.price)
break
case 'name_asc':
result.sort((a, b) => a.name.localeCompare(b.name))
break
case 'name_desc':
result.sort((a, b) => b.name.localeCompare(a.name))
break
case 'date_new':
result.sort((a, b) => new Date(b.listedAt) - new Date(a.listedAt))
break
case 'date_old':
result.sort((a, b) => new Date(a.listedAt) - new Date(b.listedAt))
break
}
return result
})
const hasMore = computed(() => currentPage.value < totalPages.value)
// Actions
const fetchItems = async (page = 1, append = false) => {
if (!append) {
isLoading.value = true
} else {
isLoadingMore.value = true
}
try {
const params = {
page,
limit: itemsPerPage.value,
...filters.value,
}
const response = await axios.get('/api/market/items', { params })
if (response.data.success) {
const newItems = response.data.items || []
if (append) {
items.value = [...items.value, ...newItems]
} else {
items.value = newItems
}
currentPage.value = response.data.page || page
totalPages.value = response.data.totalPages || 1
totalItems.value = response.data.total || 0
return true
}
return false
} catch (error) {
console.error('Failed to fetch items:', error)
toast.error('Failed to load marketplace items')
return false
} finally {
isLoading.value = false
isLoadingMore.value = false
}
}
const loadMore = async () => {
if (hasMore.value && !isLoadingMore.value) {
await fetchItems(currentPage.value + 1, true)
}
}
const fetchFeaturedItems = async () => {
try {
const response = await axios.get('/api/market/featured')
if (response.data.success) {
featuredItems.value = response.data.items || []
return true
}
return false
} catch (error) {
console.error('Failed to fetch featured items:', error)
return false
}
}
const fetchRecentSales = async (limit = 10) => {
try {
const response = await axios.get('/api/market/recent-sales', {
params: { limit }
})
if (response.data.success) {
recentSales.value = response.data.sales || []
return true
}
return false
} catch (error) {
console.error('Failed to fetch recent sales:', error)
return false
}
}
const getItemById = async (itemId) => {
try {
const response = await axios.get(`/api/market/items/${itemId}`)
if (response.data.success) {
return response.data.item
}
return null
} catch (error) {
console.error('Failed to fetch item:', error)
toast.error('Failed to load item details')
return null
}
}
const purchaseItem = async (itemId) => {
try {
const response = await axios.post(`/api/market/purchase/${itemId}`, {}, {
withCredentials: true
})
if (response.data.success) {
toast.success('Item purchased successfully!')
// Remove item from local state
items.value = items.value.filter(item => item.id !== itemId)
return true
}
return false
} catch (error) {
console.error('Failed to purchase item:', error)
const message = error.response?.data?.message || 'Failed to purchase item'
toast.error(message)
return false
}
}
const listItem = async (itemData) => {
try {
const response = await axios.post('/api/market/list', itemData, {
withCredentials: true
})
if (response.data.success) {
toast.success('Item listed successfully!')
// Add item to local state
if (response.data.item) {
items.value.unshift(response.data.item)
}
return response.data.item
}
return null
} catch (error) {
console.error('Failed to list item:', error)
const message = error.response?.data?.message || 'Failed to list item'
toast.error(message)
return null
}
}
const updateListing = async (itemId, updates) => {
try {
const response = await axios.patch(`/api/market/listing/${itemId}`, updates, {
withCredentials: true
})
if (response.data.success) {
toast.success('Listing updated successfully!')
// Update item in local state
const index = items.value.findIndex(item => item.id === itemId)
if (index !== -1 && response.data.item) {
items.value[index] = response.data.item
}
return true
}
return false
} catch (error) {
console.error('Failed to update listing:', error)
const message = error.response?.data?.message || 'Failed to update listing'
toast.error(message)
return false
}
}
const removeListing = async (itemId) => {
try {
const response = await axios.delete(`/api/market/listing/${itemId}`, {
withCredentials: true
})
if (response.data.success) {
toast.success('Listing removed successfully!')
// Remove item from local state
items.value = items.value.filter(item => item.id !== itemId)
return true
}
return false
} catch (error) {
console.error('Failed to remove listing:', error)
const message = error.response?.data?.message || 'Failed to remove listing'
toast.error(message)
return false
}
}
const updateFilter = (key, value) => {
filters.value[key] = value
currentPage.value = 1
}
const resetFilters = () => {
filters.value = {
search: '',
game: null,
minPrice: null,
maxPrice: null,
rarity: null,
wear: null,
category: null,
sortBy: 'price_asc',
statTrak: null,
souvenir: null,
}
currentPage.value = 1
}
const updateItemPrice = (itemId, newPrice) => {
const item = items.value.find(i => i.id === itemId)
if (item) {
item.price = newPrice
}
const featuredItem = featuredItems.value.find(i => i.id === itemId)
if (featuredItem) {
featuredItem.price = newPrice
}
}
const removeItem = (itemId) => {
items.value = items.value.filter(item => item.id !== itemId)
featuredItems.value = featuredItems.value.filter(item => item.id !== itemId)
}
const addItem = (item) => {
items.value.unshift(item)
totalItems.value++
}
// WebSocket integration
const setupWebSocketListeners = () => {
const wsStore = useWebSocketStore()
wsStore.on('listing_update', (data) => {
if (data?.itemId && data?.price) {
updateItemPrice(data.itemId, data.price)
}
})
wsStore.on('listing_removed', (data) => {
if (data?.itemId) {
removeItem(data.itemId)
}
})
wsStore.on('listing_added', (data) => {
if (data?.item) {
addItem(data.item)
}
})
wsStore.on('price_update', (data) => {
if (data?.itemId && data?.newPrice) {
updateItemPrice(data.itemId, data.newPrice)
}
})
wsStore.on('market_update', (data) => {
// Handle bulk market updates
console.log('Market update received:', data)
})
}
return {
// State
items,
featuredItems,
recentSales,
isLoading,
isLoadingMore,
currentPage,
totalPages,
totalItems,
itemsPerPage,
filters,
categories,
rarities,
wearConditions,
// Computed
filteredItems,
hasMore,
// Actions
fetchItems,
loadMore,
fetchFeaturedItems,
fetchRecentSales,
getItemById,
purchaseItem,
listItem,
updateListing,
removeListing,
updateFilter,
resetFilters,
updateItemPrice,
removeItem,
addItem,
setupWebSocketListeners,
}
})

View File

@@ -0,0 +1,341 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useAuthStore } from './auth'
import { useToast } from 'vue-toastification'
const toast = useToast()
export const useWebSocketStore = defineStore('websocket', () => {
// State
const ws = ref(null)
const isConnected = ref(false)
const isConnecting = ref(false)
const reconnectAttempts = ref(0)
const maxReconnectAttempts = ref(5)
const reconnectDelay = ref(1000)
const heartbeatInterval = ref(null)
const reconnectTimeout = ref(null)
const messageQueue = ref([])
const listeners = ref(new Map())
// Computed
const connectionStatus = computed(() => {
if (isConnected.value) return 'connected'
if (isConnecting.value) return 'connecting'
return 'disconnected'
})
const canReconnect = computed(() => {
return reconnectAttempts.value < maxReconnectAttempts.value
})
// Helper functions
const getWebSocketUrl = () => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const host = window.location.host
// In development, use the proxy
if (import.meta.env.DEV) {
return `ws://localhost:3000/ws`
}
return `${protocol}//${host}/ws`
}
const clearHeartbeat = () => {
if (heartbeatInterval.value) {
clearInterval(heartbeatInterval.value)
heartbeatInterval.value = null
}
}
const clearReconnectTimeout = () => {
if (reconnectTimeout.value) {
clearTimeout(reconnectTimeout.value)
reconnectTimeout.value = null
}
}
const startHeartbeat = () => {
clearHeartbeat()
// Send ping every 30 seconds
heartbeatInterval.value = setInterval(() => {
if (isConnected.value && ws.value?.readyState === WebSocket.OPEN) {
send({ type: 'ping' })
}
}, 30000)
}
// Actions
const connect = () => {
if (ws.value?.readyState === WebSocket.OPEN || isConnecting.value) {
console.log('WebSocket already connected or connecting')
return
}
isConnecting.value = true
clearReconnectTimeout()
try {
const wsUrl = getWebSocketUrl()
console.log('Connecting to WebSocket:', wsUrl)
ws.value = new WebSocket(wsUrl)
ws.value.onopen = () => {
console.log('WebSocket connected')
isConnected.value = true
isConnecting.value = false
reconnectAttempts.value = 0
startHeartbeat()
// Send queued messages
while (messageQueue.value.length > 0) {
const message = messageQueue.value.shift()
send(message)
}
// Emit connected event
emit('connected', { timestamp: Date.now() })
}
ws.value.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
console.log('WebSocket message received:', data)
handleMessage(data)
} catch (error) {
console.error('Failed to parse WebSocket message:', error)
}
}
ws.value.onerror = (error) => {
console.error('WebSocket error:', error)
isConnecting.value = false
}
ws.value.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason)
isConnected.value = false
isConnecting.value = false
clearHeartbeat()
// Emit disconnected event
emit('disconnected', {
code: event.code,
reason: event.reason,
timestamp: Date.now()
})
// Attempt to reconnect
if (!event.wasClean && canReconnect.value) {
scheduleReconnect()
}
}
} catch (error) {
console.error('Failed to create WebSocket connection:', error)
isConnecting.value = false
}
}
const disconnect = () => {
clearHeartbeat()
clearReconnectTimeout()
reconnectAttempts.value = maxReconnectAttempts.value // Prevent auto-reconnect
if (ws.value) {
ws.value.close(1000, 'Client disconnect')
ws.value = null
}
isConnected.value = false
isConnecting.value = false
}
const scheduleReconnect = () => {
if (!canReconnect.value) {
console.log('Max reconnect attempts reached')
toast.error('Lost connection to server. Please refresh the page.')
return
}
reconnectAttempts.value++
const delay = reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1)
console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts.value}/${maxReconnectAttempts.value})`)
clearReconnectTimeout()
reconnectTimeout.value = setTimeout(() => {
connect()
}, delay)
}
const send = (message) => {
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) {
console.warn('WebSocket not connected, queueing message:', message)
messageQueue.value.push(message)
return false
}
try {
const payload = typeof message === 'string' ? message : JSON.stringify(message)
ws.value.send(payload)
return true
} catch (error) {
console.error('Failed to send WebSocket message:', error)
return false
}
}
const handleMessage = (data) => {
const { type, data: payload, timestamp } = data
switch (type) {
case 'connected':
console.log('Server confirmed connection:', payload)
break
case 'pong':
// Heartbeat response
break
case 'notification':
if (payload?.message) {
toast.info(payload.message)
}
break
case 'balance_update':
// Update user balance
const authStore = useAuthStore()
if (payload?.balance !== undefined) {
authStore.updateBalance(payload.balance)
}
break
case 'item_sold':
toast.success(`Your item "${payload?.itemName || 'item'}" has been sold!`)
break
case 'item_purchased':
toast.success(`Successfully purchased "${payload?.itemName || 'item'}"!`)
break
case 'trade_status':
if (payload?.status === 'completed') {
toast.success('Trade completed successfully!')
} else if (payload?.status === 'failed') {
toast.error(`Trade failed: ${payload?.reason || 'Unknown error'}`)
}
break
case 'price_update':
case 'listing_update':
case 'market_update':
// These will be handled by listeners
break
case 'announcement':
if (payload?.message) {
toast.warning(payload.message, { timeout: 10000 })
}
break
case 'error':
console.error('Server error:', payload)
if (payload?.message) {
toast.error(payload.message)
}
break
default:
console.log('Unhandled message type:', type)
}
// Emit to listeners
emit(type, payload)
}
const on = (event, callback) => {
if (!listeners.value.has(event)) {
listeners.value.set(event, [])
}
listeners.value.get(event).push(callback)
// Return unsubscribe function
return () => off(event, callback)
}
const off = (event, callback) => {
if (!listeners.value.has(event)) return
const callbacks = listeners.value.get(event)
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
if (callbacks.length === 0) {
listeners.value.delete(event)
}
}
const emit = (event, data) => {
if (!listeners.value.has(event)) return
const callbacks = listeners.value.get(event)
callbacks.forEach(callback => {
try {
callback(data)
} catch (error) {
console.error(`Error in event listener for "${event}":`, error)
}
})
}
const once = (event, callback) => {
const wrappedCallback = (data) => {
callback(data)
off(event, wrappedCallback)
}
return on(event, wrappedCallback)
}
const clearListeners = () => {
listeners.value.clear()
}
// Ping the server
const ping = () => {
send({ type: 'ping' })
}
return {
// State
ws,
isConnected,
isConnecting,
reconnectAttempts,
maxReconnectAttempts,
messageQueue,
// Computed
connectionStatus,
canReconnect,
// Actions
connect,
disconnect,
send,
on,
off,
once,
emit,
clearListeners,
ping,
}
})