- Add user management system with all CRUD operations - Add promotion statistics dashboard with export - Simplify Trading & Market settings UI - Fix promotion schema (dates now optional) - Add missing API endpoints and PATCH support - Add comprehensive documentation - Fix critical bugs (deletePromotion, duplicate endpoints) All features tested and production-ready.
1297 lines
32 KiB
Vue
1297 lines
32 KiB
Vue
<template>
|
||
<div class="admin-debug-panel">
|
||
<!-- Confirm Delete Modal -->
|
||
<ConfirmModal
|
||
v-model="showDeleteModal"
|
||
title="Delete Announcement"
|
||
:message="`Are you sure you want to delete this announcement?`"
|
||
:details="announcementToDelete?.message"
|
||
confirm-text="Delete"
|
||
cancel-text="Cancel"
|
||
type="danger"
|
||
:loading="deleting"
|
||
@confirm="confirmDelete"
|
||
/>
|
||
|
||
<div class="debug-header">
|
||
<h2>🔧 Admin Debug Panel</h2>
|
||
<button @click="runAllTests" class="btn-test" :disabled="testing">
|
||
<Loader2 v-if="testing" class="animate-spin" />
|
||
<span>{{ testing ? "Testing..." : "Run All Tests" }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tab Navigation -->
|
||
<div class="tabs">
|
||
<button
|
||
v-for="tab in tabs"
|
||
:key="tab.id"
|
||
@click="activeTab = tab.id"
|
||
class="tab"
|
||
:class="{ active: activeTab === tab.id }"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Authentication Tab -->
|
||
<div v-if="activeTab === 'auth'" class="tab-content">
|
||
<div class="debug-section">
|
||
<h3>Authentication Status</h3>
|
||
<div class="status-grid">
|
||
<div class="status-item">
|
||
<span class="label">Logged In:</span>
|
||
<span
|
||
class="value"
|
||
:class="authStore.isAuthenticated ? 'success' : 'error'"
|
||
>
|
||
{{ authStore.isAuthenticated ? "✓ Yes" : "✗ No" }}
|
||
</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Username:</span>
|
||
<span class="value">{{ authStore.username || "N/A" }}</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Steam ID:</span>
|
||
<span class="value">{{ authStore.steamId || "N/A" }}</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Staff Level:</span>
|
||
<span class="value" :class="getStaffLevelClass()">
|
||
{{ authStore.staffLevel }}
|
||
</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Is Admin:</span>
|
||
<span
|
||
class="value"
|
||
:class="authStore.isAdmin ? 'success' : 'error'"
|
||
>
|
||
{{ authStore.isAdmin ? "✓ Yes" : "✗ No" }}
|
||
</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Balance:</span>
|
||
<span class="value">${{ authStore.balance.toFixed(2) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="debug-section">
|
||
<h3>Backend Connectivity</h3>
|
||
<div class="test-results">
|
||
<div v-for="test in tests" :key="test.name" class="test-item">
|
||
<div class="test-header">
|
||
<span class="test-name">{{ test.name }}</span>
|
||
<span class="test-status" :class="test.status">
|
||
<Loader2
|
||
v-if="test.status === 'running'"
|
||
class="animate-spin"
|
||
/>
|
||
<CheckCircle v-else-if="test.status === 'success'" />
|
||
<XCircle v-else-if="test.status === 'error'" />
|
||
<Clock v-else />
|
||
</span>
|
||
</div>
|
||
<div v-if="test.message" class="test-message" :class="test.status">
|
||
{{ test.message }}
|
||
</div>
|
||
<div v-if="test.details" class="test-details">
|
||
<pre>{{ JSON.stringify(test.details, null, 2) }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Announcements Tab -->
|
||
<div v-if="activeTab === 'announcements'" class="tab-content">
|
||
<div class="debug-section">
|
||
<div class="section-header">
|
||
<h3>📢 Announcements Testing</h3>
|
||
<button @click="loadAnnouncements" class="btn-action">
|
||
<RefreshCw class="icon" />
|
||
Reload
|
||
</button>
|
||
</div>
|
||
|
||
<div class="status-grid">
|
||
<div class="status-item">
|
||
<span class="label">Total Loaded:</span>
|
||
<span class="value">{{ announcements.length }}</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Active:</span>
|
||
<span class="value success">{{ activeAnnouncements.length }}</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Dismissed:</span>
|
||
<span class="value">{{ dismissedIds.length }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions-row">
|
||
<button @click="clearDismissed" class="btn-action">
|
||
<Trash2 class="icon" />
|
||
Clear Dismissed
|
||
</button>
|
||
<button @click="testAnnouncementAPI" class="btn-action">
|
||
<TestTube class="icon" />
|
||
Test API
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="announcements.length === 0" class="empty-state">
|
||
⚠️ No announcements loaded. Check API or create one in the admin
|
||
panel.
|
||
</div>
|
||
|
||
<div v-else class="announcement-list">
|
||
<div
|
||
v-for="announcement in announcements"
|
||
:key="announcement.id"
|
||
class="announcement-debug-item"
|
||
:class="`type-${announcement.type}`"
|
||
>
|
||
<div class="announcement-row">
|
||
<span class="announcement-type">{{ announcement.type }}</span>
|
||
<span class="announcement-enabled">
|
||
{{ announcement.enabled ? "✅ Enabled" : "❌ Disabled" }}
|
||
</span>
|
||
<span class="announcement-dismissible">
|
||
{{
|
||
announcement.dismissible ? "👋 Dismissible" : "🔒 Permanent"
|
||
}}
|
||
</span>
|
||
<button
|
||
@click="deleteAnnouncement(announcement)"
|
||
class="btn-delete-inline"
|
||
title="Delete announcement"
|
||
>
|
||
<Trash2 :size="16" />
|
||
</button>
|
||
</div>
|
||
<div class="announcement-message">{{ announcement.message }}</div>
|
||
<div class="announcement-meta">
|
||
<span>ID: {{ announcement.id.substring(0, 8) }}...</span>
|
||
<span v-if="announcement.startDate"
|
||
>Start: {{ formatDate(announcement.startDate) }}</span
|
||
>
|
||
<span v-if="announcement.endDate"
|
||
>End: {{ formatDate(announcement.endDate) }}</span
|
||
>
|
||
<span
|
||
v-if="dismissedIds.includes(announcement.id)"
|
||
class="dismissed-badge"
|
||
>DISMISSED</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="apiResponse" class="debug-section">
|
||
<h3>Last API Response</h3>
|
||
<div class="test-details">
|
||
<pre>{{ JSON.stringify(apiResponse, null, 2) }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Maintenance Tab -->
|
||
<div v-if="activeTab === 'maintenance'" class="tab-content">
|
||
<div class="debug-section">
|
||
<div class="section-header">
|
||
<h3>🔧 Maintenance Mode Testing</h3>
|
||
<button @click="loadMaintenanceStatus" class="btn-action">
|
||
<RefreshCw class="icon" />
|
||
Reload
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="maintenanceInfo" class="maintenance-status">
|
||
<div
|
||
class="status-card"
|
||
:class="maintenanceInfo.enabled ? 'warning' : 'success'"
|
||
>
|
||
<div class="status-icon">
|
||
{{ maintenanceInfo.enabled ? "⚠️" : "✅" }}
|
||
</div>
|
||
<div class="status-content">
|
||
<h4>
|
||
{{
|
||
maintenanceInfo.enabled
|
||
? "MAINTENANCE MODE ACTIVE"
|
||
: "Site Operational"
|
||
}}
|
||
</h4>
|
||
<p>{{ maintenanceInfo.message }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-grid">
|
||
<div class="status-item">
|
||
<span class="label">Maintenance Enabled:</span>
|
||
<span
|
||
class="value"
|
||
:class="maintenanceInfo.enabled ? 'error' : 'success'"
|
||
>
|
||
{{ maintenanceInfo.enabled ? "Yes" : "No" }}
|
||
</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Scheduled End:</span>
|
||
<span class="value">
|
||
{{
|
||
maintenanceInfo.scheduledEnd
|
||
? formatDate(maintenanceInfo.scheduledEnd)
|
||
: "None"
|
||
}}
|
||
</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span class="label">Whitelisted Users:</span>
|
||
<span class="value">{{
|
||
maintenanceInfo.allowedSteamIds?.length || 0
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="actions-row">
|
||
<button @click="testMaintenanceBypass" class="btn-action">
|
||
<Shield class="icon" />
|
||
Test Admin Bypass
|
||
</button>
|
||
<button @click="testMaintenanceBlock" class="btn-action">
|
||
<XCircle class="icon" />
|
||
Test User Block
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="maintenanceTests.length > 0" class="debug-section">
|
||
<h3>Test Results</h3>
|
||
<div class="test-results">
|
||
<div
|
||
v-for="test in maintenanceTests"
|
||
:key="test.name"
|
||
class="test-item"
|
||
>
|
||
<div class="test-header">
|
||
<span class="test-name">{{ test.name }}</span>
|
||
<span class="test-status" :class="test.status">
|
||
<CheckCircle v-if="test.status === 'success'" />
|
||
<XCircle v-else-if="test.status === 'error'" />
|
||
<Clock v-else />
|
||
</span>
|
||
</div>
|
||
<div v-if="test.message" class="test-message" :class="test.status">
|
||
{{ test.message }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- System Tab -->
|
||
<div v-if="activeTab === 'system'" class="tab-content"></div>
|
||
|
||
<div class="debug-section">
|
||
<h3>Environment Info</h3>
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span class="label">API Base URL:</span>
|
||
<span class="value">{{ apiBaseUrl }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">Current Route:</span>
|
||
<span class="value">{{ currentRoute }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">Window Location:</span>
|
||
<span class="value">{{ windowLocation }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">User Agent:</span>
|
||
<span class="value small">{{ userAgent }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="debug-section">
|
||
<h3>Quick Actions</h3>
|
||
<div class="actions-grid">
|
||
<button @click="refreshAuth" class="btn-action">
|
||
<RefreshCw class="icon" />
|
||
Refresh Auth
|
||
</button>
|
||
<button @click="clearCache" class="btn-action">
|
||
<Trash2 class="icon" />
|
||
Clear Cache
|
||
</button>
|
||
<button @click="testAdminRoute" class="btn-action">
|
||
<Shield class="icon" />
|
||
Test Admin Route
|
||
</button>
|
||
<button @click="copyDebugInfo" class="btn-action">
|
||
<Copy class="icon" />
|
||
Copy Debug Info
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="errorLog.length > 0" class="debug-section">
|
||
<h3>Error Log</h3>
|
||
<div class="error-log">
|
||
<div v-for="(error, index) in errorLog" :key="index" class="error-item">
|
||
<span class="error-time">{{ error.time }}</span>
|
||
<span class="error-message">{{ error.message }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted } from "vue";
|
||
import { useRouter } from "vue-router";
|
||
import { useAuthStore } from "@/stores/auth";
|
||
import { useToast } from "vue-toastification";
|
||
import axios from "@/utils/axios";
|
||
import ConfirmModal from "@/components/ConfirmModal.vue";
|
||
import {
|
||
Loader2,
|
||
CheckCircle,
|
||
XCircle,
|
||
Clock,
|
||
RefreshCw,
|
||
Trash2,
|
||
Shield,
|
||
Copy,
|
||
TestTube,
|
||
} from "lucide-vue-next";
|
||
|
||
const router = useRouter();
|
||
const authStore = useAuthStore();
|
||
const toast = useToast();
|
||
|
||
const testing = ref(false);
|
||
const errorLog = ref([]);
|
||
const activeTab = ref("auth");
|
||
const announcements = ref([]);
|
||
const dismissedIds = ref([]);
|
||
const apiResponse = ref(null);
|
||
const maintenanceInfo = ref(null);
|
||
const maintenanceTests = ref([]);
|
||
const showDeleteModal = ref(false);
|
||
const announcementToDelete = ref(null);
|
||
const deleting = ref(false);
|
||
|
||
const tabs = [
|
||
{ id: "auth", label: "🔐 Auth & Connectivity" },
|
||
{ id: "announcements", label: "📢 Announcements" },
|
||
{ id: "maintenance", label: "🔧 Maintenance" },
|
||
{ id: "system", label: "⚙️ System" },
|
||
];
|
||
|
||
const tests = ref([
|
||
{ name: "Health Check", status: "pending", message: "", details: null },
|
||
{ name: "Auth Check", status: "pending", message: "", details: null },
|
||
{
|
||
name: "Admin Config Access",
|
||
status: "pending",
|
||
message: "",
|
||
details: null,
|
||
},
|
||
{
|
||
name: "Admin Routes Access",
|
||
status: "pending",
|
||
message: "",
|
||
details: null,
|
||
},
|
||
]);
|
||
|
||
const apiBaseUrl = computed(() => {
|
||
return import.meta.env.VITE_API_URL || "/api";
|
||
});
|
||
const currentRoute = computed(() => router.currentRoute.value.path);
|
||
const windowLocation = computed(() => window.location.href);
|
||
const userAgent = computed(() => navigator.userAgent);
|
||
|
||
const activeAnnouncements = computed(() => {
|
||
return announcements.value.filter((announcement) => {
|
||
if (dismissedIds.value.includes(announcement.id)) return false;
|
||
if (!announcement.enabled) return false;
|
||
|
||
const now = new Date();
|
||
if (announcement.startDate && now < new Date(announcement.startDate))
|
||
return false;
|
||
if (announcement.endDate && now > new Date(announcement.endDate))
|
||
return false;
|
||
|
||
return true;
|
||
});
|
||
});
|
||
|
||
const getStaffLevelClass = () => {
|
||
const level = authStore.staffLevel;
|
||
if (level >= 5) return "admin-super";
|
||
if (level >= 3) return "admin";
|
||
if (level >= 2) return "moderator";
|
||
if (level >= 1) return "staff";
|
||
return "user";
|
||
};
|
||
|
||
const logError = (message) => {
|
||
const now = new Date();
|
||
errorLog.value.unshift({
|
||
time: now.toLocaleTimeString(),
|
||
message: message,
|
||
});
|
||
if (errorLog.value.length > 10) {
|
||
errorLog.value.pop();
|
||
}
|
||
};
|
||
|
||
const formatDate = (date) => {
|
||
if (!date) return "N/A";
|
||
return new Date(date).toLocaleString("en-US", {
|
||
month: "short",
|
||
day: "numeric",
|
||
hour: "numeric",
|
||
minute: "2-digit",
|
||
});
|
||
};
|
||
|
||
// Announcements functions
|
||
const loadAnnouncements = async () => {
|
||
try {
|
||
console.log("📡 Debug: Fetching announcements...");
|
||
const response = await axios.get("/api/config/announcements");
|
||
console.log("📡 Debug: Response:", response);
|
||
|
||
apiResponse.value = response.data;
|
||
|
||
if (response.data.success) {
|
||
announcements.value = response.data.announcements;
|
||
console.log("✅ Debug: Loaded announcements:", announcements.value);
|
||
toast.success(`Loaded ${announcements.value.length} announcements`);
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ Debug: Failed to load announcements:", error);
|
||
toast.error("Failed to load announcements");
|
||
logError("Load announcements: " + error.message);
|
||
}
|
||
};
|
||
|
||
const clearDismissed = () => {
|
||
dismissedIds.value = [];
|
||
localStorage.removeItem("dismissedAnnouncements");
|
||
toast.success("Cleared dismissed announcements");
|
||
console.log("🗑️ Debug: Cleared dismissed");
|
||
};
|
||
|
||
const testAnnouncementAPI = async () => {
|
||
try {
|
||
const response = await fetch(
|
||
"http://localhost:3000/api/config/announcements"
|
||
);
|
||
const data = await response.json();
|
||
apiResponse.value = { direct: true, ...data };
|
||
toast.success("Direct API test successful");
|
||
console.log("🧪 Debug: Direct fetch result:", data);
|
||
} catch (error) {
|
||
apiResponse.value = { error: error.message };
|
||
toast.error("Direct API test failed");
|
||
console.error("🧪 Debug: Direct fetch error:", error);
|
||
}
|
||
};
|
||
|
||
// Maintenance functions
|
||
const loadMaintenanceStatus = async () => {
|
||
try {
|
||
const response = await axios.get("/api/config/public");
|
||
if (response.data.success) {
|
||
maintenanceInfo.value = response.data.config.maintenance;
|
||
toast.success("Maintenance status loaded");
|
||
}
|
||
} catch (error) {
|
||
toast.error("Failed to load maintenance status");
|
||
logError("Load maintenance: " + error.message);
|
||
}
|
||
};
|
||
|
||
const testMaintenanceBypass = async () => {
|
||
maintenanceTests.value = [];
|
||
try {
|
||
const response = await axios.get("/api/admin/config");
|
||
maintenanceTests.value.push({
|
||
name: "Admin Bypass Test",
|
||
status: "success",
|
||
message: "✅ Admin can access during maintenance",
|
||
});
|
||
toast.success("Admin bypass working!");
|
||
} catch (error) {
|
||
maintenanceTests.value.push({
|
||
name: "Admin Bypass Test",
|
||
status: "error",
|
||
message:
|
||
"❌ Admin blocked: " + (error.response?.data?.message || error.message),
|
||
});
|
||
toast.error("Admin bypass failed!");
|
||
}
|
||
};
|
||
|
||
const testMaintenanceBlock = () => {
|
||
maintenanceTests.value.push({
|
||
name: "User Block Test",
|
||
status: "success",
|
||
message: "ℹ️ Test in incognito window to verify non-admin blocking",
|
||
});
|
||
toast.info("Open incognito window to test user blocking");
|
||
};
|
||
|
||
// Delete announcement
|
||
const deleteAnnouncement = (announcement) => {
|
||
announcementToDelete.value = announcement;
|
||
showDeleteModal.value = true;
|
||
};
|
||
|
||
const confirmDelete = async () => {
|
||
if (!announcementToDelete.value) return;
|
||
|
||
deleting.value = true;
|
||
|
||
try {
|
||
const announcementId = announcementToDelete.value.id;
|
||
const deleteUrl = `/api/admin/announcements/${announcementId}`;
|
||
|
||
console.log("🗑️ Deleting announcement ID:", announcementId);
|
||
console.log("🗑️ Delete URL:", deleteUrl);
|
||
console.log(
|
||
"🗑️ Axios instance baseURL:",
|
||
import.meta.env.VITE_API_URL || "/api"
|
||
);
|
||
|
||
const response = await axios.delete(deleteUrl);
|
||
|
||
console.log("✅ Delete response:", response.data);
|
||
|
||
if (response.data.success) {
|
||
toast.success("Announcement deleted successfully");
|
||
showDeleteModal.value = false;
|
||
announcementToDelete.value = null;
|
||
|
||
// Reload announcements
|
||
await loadAnnouncements();
|
||
} else {
|
||
toast.error(response.data.message || "Failed to delete announcement");
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ Failed to delete announcement:", error);
|
||
console.error("❌ Error response:", error.response);
|
||
console.error("❌ Error config:", error.config);
|
||
|
||
const errorMessage =
|
||
error.response?.data?.message ||
|
||
error.message ||
|
||
"Failed to delete announcement";
|
||
|
||
toast.error(errorMessage);
|
||
logError("Delete announcement: " + errorMessage);
|
||
} finally {
|
||
deleting.value = false;
|
||
}
|
||
};
|
||
|
||
const runAllTests = async () => {
|
||
testing.value = true;
|
||
|
||
// Reset all tests
|
||
tests.value.forEach((test) => {
|
||
test.status = "pending";
|
||
test.message = "";
|
||
test.details = null;
|
||
});
|
||
|
||
// Test 1: Health Check
|
||
tests.value[0].status = "running";
|
||
try {
|
||
// Use relative path - will go through Vite proxy in dev
|
||
const response = await axios.get("/api/health", {
|
||
timeout: 5000,
|
||
});
|
||
tests.value[0].status = "success";
|
||
tests.value[0].message = "Backend is running";
|
||
tests.value[0].details = response.data;
|
||
} catch (error) {
|
||
tests.value[0].status = "error";
|
||
tests.value[0].message = error.message || "Backend not accessible";
|
||
logError("Health check failed: " + error.message);
|
||
}
|
||
|
||
// Test 2: Auth Check
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
tests.value[1].status = "running";
|
||
try {
|
||
const response = await axios.get("/api/auth/me");
|
||
tests.value[1].status = "success";
|
||
tests.value[1].message = `Authenticated as ${response.data.user.username}`;
|
||
tests.value[1].details = {
|
||
steamId: response.data.user.steamId,
|
||
staffLevel: response.data.user.staffLevel,
|
||
isAdmin: response.data.user.staffLevel >= 3,
|
||
};
|
||
} catch (error) {
|
||
tests.value[1].status = "error";
|
||
tests.value[1].message = "Not authenticated or session expired";
|
||
tests.value[1].details = {
|
||
error: error.response?.data?.message || error.message,
|
||
};
|
||
logError("Auth check failed: " + error.message);
|
||
}
|
||
|
||
// Test 3: Admin Config Access
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
tests.value[2].status = "running";
|
||
try {
|
||
const response = await axios.get("/api/admin/config");
|
||
tests.value[2].status = "success";
|
||
tests.value[2].message = "Admin config accessible";
|
||
tests.value[2].details = {
|
||
hasConfig: !!response.data.config,
|
||
maintenance: response.data.config?.maintenance?.enabled,
|
||
trading: response.data.config?.trading?.enabled,
|
||
};
|
||
} catch (error) {
|
||
tests.value[2].status = "error";
|
||
tests.value[2].message =
|
||
error.response?.data?.message || "Access denied or route not found";
|
||
tests.value[2].details = {
|
||
status: error.response?.status,
|
||
error: error.response?.data?.message || error.message,
|
||
};
|
||
logError("Admin config access failed: " + error.message);
|
||
}
|
||
|
||
// Test 4: Admin Routes
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
tests.value[3].status = "running";
|
||
try {
|
||
const response = await axios.get(
|
||
"/api/admin/users/search?query=test&limit=1"
|
||
);
|
||
tests.value[3].status = "success";
|
||
tests.value[3].message = "Admin user routes accessible";
|
||
tests.value[3].details = {
|
||
usersFound: response.data.users?.length || 0,
|
||
};
|
||
} catch (error) {
|
||
tests.value[3].status = "error";
|
||
tests.value[3].message =
|
||
error.response?.data?.message || "Admin routes not accessible";
|
||
tests.value[3].details = {
|
||
status: error.response?.status,
|
||
error: error.response?.data?.message || error.message,
|
||
};
|
||
logError("Admin routes test failed: " + error.message);
|
||
}
|
||
|
||
testing.value = false;
|
||
};
|
||
|
||
const refreshAuth = async () => {
|
||
try {
|
||
await authStore.fetchUser();
|
||
toast.success("Auth refreshed");
|
||
} catch (error) {
|
||
toast.error("Failed to refresh auth");
|
||
logError("Refresh auth failed: " + error.message);
|
||
}
|
||
};
|
||
|
||
const clearCache = () => {
|
||
try {
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
toast.success("Cache cleared - please refresh page");
|
||
} catch (error) {
|
||
toast.error("Failed to clear cache");
|
||
logError("Clear cache failed: " + error.message);
|
||
}
|
||
};
|
||
|
||
const testAdminRoute = async () => {
|
||
try {
|
||
const response = await axios.get("/api/admin/config");
|
||
toast.success("Admin route accessible!");
|
||
console.log("Admin config:", response.data);
|
||
} catch (error) {
|
||
toast.error(
|
||
"Admin route failed: " + (error.response?.data?.message || error.message)
|
||
);
|
||
logError("Admin route test failed: " + error.message);
|
||
console.error("Error:", error);
|
||
}
|
||
};
|
||
|
||
const copyDebugInfo = () => {
|
||
const debugInfo = {
|
||
timestamp: new Date().toISOString(),
|
||
auth: {
|
||
isAuthenticated: authStore.isAuthenticated,
|
||
username: authStore.username,
|
||
steamId: authStore.steamId,
|
||
staffLevel: authStore.staffLevel,
|
||
isAdmin: authStore.isAdmin,
|
||
},
|
||
environment: {
|
||
apiBaseUrl: apiBaseUrl.value,
|
||
currentRoute: currentRoute.value,
|
||
location: windowLocation.value,
|
||
},
|
||
tests: tests.value,
|
||
errors: errorLog.value,
|
||
};
|
||
|
||
navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2));
|
||
toast.success("Debug info copied to clipboard");
|
||
};
|
||
|
||
// Watch for tab changes and load appropriate data
|
||
watch(activeTab, (newTab) => {
|
||
console.log("🔄 Debug panel tab changed to:", newTab);
|
||
|
||
if (newTab === "auth") {
|
||
runAllTests();
|
||
} else if (newTab === "announcements") {
|
||
loadAnnouncements();
|
||
} else if (newTab === "maintenance") {
|
||
loadMaintenanceStatus();
|
||
}
|
||
});
|
||
|
||
onMounted(() => {
|
||
// Load dismissed IDs
|
||
const stored = localStorage.getItem("dismissedAnnouncements");
|
||
if (stored) {
|
||
try {
|
||
dismissedIds.value = JSON.parse(stored);
|
||
} catch (e) {
|
||
console.error("Failed to parse dismissed announcements", e);
|
||
}
|
||
}
|
||
|
||
// Auto-run tests for initial tab
|
||
setTimeout(() => {
|
||
if (activeTab.value === "auth") {
|
||
runAllTests();
|
||
} else if (activeTab.value === "announcements") {
|
||
loadAnnouncements();
|
||
} else if (activeTab.value === "maintenance") {
|
||
loadMaintenanceStatus();
|
||
}
|
||
}, 500);
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.admin-debug-panel {
|
||
background: #1f2937;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1.5rem;
|
||
border-bottom: 2px solid #374151;
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.tab {
|
||
padding: 0.75rem 1.5rem;
|
||
background: transparent;
|
||
color: #9ca3af;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
margin-bottom: -2px;
|
||
}
|
||
|
||
.tab:hover {
|
||
color: #e5e7eb;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.tab.active {
|
||
color: #60a5fa;
|
||
border-bottom-color: #60a5fa;
|
||
}
|
||
|
||
.tab-content {
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.debug-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1.5rem;
|
||
padding-bottom: 1rem;
|
||
border-bottom: 2px solid #374151;
|
||
}
|
||
|
||
.debug-header h2 {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: #f59e0b;
|
||
}
|
||
|
||
.btn-test {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 1rem;
|
||
background: #3b82f6;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0.375rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn-test:hover:not(:disabled) {
|
||
background: #2563eb;
|
||
}
|
||
|
||
.btn-test:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.debug-section {
|
||
margin-bottom: 1.5rem;
|
||
padding: 1rem;
|
||
background: #111827;
|
||
border-radius: 0.375rem;
|
||
border: 1px solid #374151;
|
||
}
|
||
|
||
.debug-section h3 {
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
margin-bottom: 1rem;
|
||
color: #60a5fa;
|
||
}
|
||
|
||
.status-grid,
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.status-item,
|
||
.info-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0.5rem;
|
||
background: #1f2937;
|
||
border-radius: 0.25rem;
|
||
border: 1px solid #374151;
|
||
}
|
||
|
||
.label {
|
||
font-weight: 600;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.value {
|
||
font-weight: 500;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
.value.small {
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.value.success {
|
||
color: #10b981;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.value.error {
|
||
color: #ef4444;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.value.admin-super {
|
||
color: #a855f7;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.value.admin {
|
||
color: #3b82f6;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.value.moderator {
|
||
color: #10b981;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.value.staff {
|
||
color: #f59e0b;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.test-results {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.test-item {
|
||
padding: 0.75rem;
|
||
background: #1f2937;
|
||
border-radius: 0.375rem;
|
||
border: 1px solid #374151;
|
||
}
|
||
|
||
.test-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.test-name {
|
||
font-weight: 600;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
.test-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
.test-status.success {
|
||
color: #10b981;
|
||
}
|
||
|
||
.test-status.error {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.test-status.running {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.test-status.pending {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.test-message {
|
||
font-size: 0.875rem;
|
||
padding: 0.5rem;
|
||
border-radius: 0.25rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.test-message.success {
|
||
color: #10b981;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
}
|
||
|
||
.test-message.error {
|
||
color: #ef4444;
|
||
background: rgba(239, 68, 68, 0.1);
|
||
}
|
||
|
||
.test-details {
|
||
font-size: 0.75rem;
|
||
padding: 0.5rem;
|
||
background: #111827;
|
||
border-radius: 0.25rem;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.test-details pre {
|
||
margin: 0;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.actions-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.actions-row {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
margin: 1rem 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.section-header h3 {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.btn-action {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem;
|
||
background: #374151;
|
||
color: #e5e7eb;
|
||
border: 1px solid #4b5563;
|
||
border-radius: 0.375rem;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-action:hover {
|
||
background: #4b5563;
|
||
border-color: #6b7280;
|
||
}
|
||
|
||
.btn-action .icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.error-log {
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.error-item {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
padding: 0.5rem;
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border-left: 3px solid #ef4444;
|
||
border-radius: 0.25rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.error-time {
|
||
color: #9ca3af;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.error-message {
|
||
color: #ef4444;
|
||
}
|
||
|
||
.animate-spin {
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* Announcements specific styles */
|
||
.announcement-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.announcement-debug-item {
|
||
padding: 1rem;
|
||
border-radius: 0.375rem;
|
||
border: 1px solid #374151;
|
||
background: #1f2937;
|
||
}
|
||
|
||
.type-info {
|
||
border-left: 4px solid #3b82f6;
|
||
}
|
||
|
||
.type-warning {
|
||
border-left: 4px solid #f59e0b;
|
||
}
|
||
|
||
.type-success {
|
||
border-left: 4px solid #10b981;
|
||
}
|
||
|
||
.type-error {
|
||
border-left: 4px solid #ef4444;
|
||
}
|
||
|
||
.announcement-row {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
margin-bottom: 0.5rem;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.announcement-type {
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
font-size: 0.75rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.announcement-enabled {
|
||
font-size: 0.75rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: #10b981;
|
||
}
|
||
|
||
.announcement-dismissible {
|
||
font-size: 0.75rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: 0.25rem;
|
||
background: rgba(59, 130, 246, 0.1);
|
||
color: #60a5fa;
|
||
}
|
||
|
||
.announcement-message {
|
||
color: #e5e7eb;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.announcement-meta {
|
||
display: flex;
|
||
gap: 1rem;
|
||
font-size: 0.75rem;
|
||
color: #9ca3af;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.dismissed-badge {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #ef4444;
|
||
padding: 0.125rem 0.375rem;
|
||
border-radius: 0.25rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.empty-state {
|
||
padding: 2rem;
|
||
text-align: center;
|
||
color: #f59e0b;
|
||
background: rgba(245, 158, 11, 0.1);
|
||
border-radius: 0.375rem;
|
||
border: 1px dashed #f59e0b;
|
||
}
|
||
|
||
/* Maintenance specific styles */
|
||
.maintenance-status {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.status-card {
|
||
display: flex;
|
||
gap: 1rem;
|
||
padding: 1.5rem;
|
||
border-radius: 0.5rem;
|
||
border: 2px solid;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.status-card.warning {
|
||
background: rgba(245, 158, 11, 0.1);
|
||
border-color: #f59e0b;
|
||
}
|
||
|
||
.status-card.success {
|
||
background: rgba(16, 185, 129, 0.1);
|
||
border-color: #10b981;
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 2.5rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
.status-content h4 {
|
||
margin: 0 0 0.5rem 0;
|
||
font-size: 1.25rem;
|
||
font-weight: 700;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
.status-content p {
|
||
margin: 0;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.btn-delete-inline {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0.375rem;
|
||
background: rgba(239, 68, 68, 0.1);
|
||
color: #ef4444;
|
||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-delete-inline:hover {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
border-color: rgba(239, 68, 68, 0.5);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.btn-delete-inline:active {
|
||
transform: scale(0.95);
|
||
}
|
||
</style>
|