feat: Complete admin panel implementation
- 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.
This commit is contained in:
531
frontend/src/views/BannedPage.vue
Normal file
531
frontend/src/views/BannedPage.vue
Normal file
@@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<div class="banned-page">
|
||||
<div class="banned-container">
|
||||
<!-- Icon -->
|
||||
<div class="icon-wrapper">
|
||||
<ShieldAlert class="banned-icon" />
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="banned-title">Account Suspended</h1>
|
||||
|
||||
<!-- Message -->
|
||||
<p class="banned-message">
|
||||
Your account has been suspended due to a violation of our Terms of
|
||||
Service.
|
||||
</p>
|
||||
|
||||
<!-- Ban Details -->
|
||||
<div v-if="banInfo" class="ban-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Reason:</span>
|
||||
<span class="detail-value">{{
|
||||
banInfo.reason || "Violation of Terms of Service"
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="banInfo.bannedAt" class="detail-item">
|
||||
<span class="detail-label">Banned on:</span>
|
||||
<span class="detail-value">{{ formatDate(banInfo.bannedAt) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="banInfo.bannedUntil" class="detail-item">
|
||||
<span class="detail-label">Ban expires:</span>
|
||||
<span class="detail-value">{{
|
||||
formatDate(banInfo.bannedUntil)
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="detail-item permanent-ban">
|
||||
<AlertCircle :size="18" />
|
||||
<span>This is a permanent ban</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="info-box">
|
||||
<Info :size="20" class="info-icon" />
|
||||
<div class="info-content">
|
||||
<p class="info-title">What does this mean?</p>
|
||||
<p class="info-text">
|
||||
You will not be able to access your account, make trades, or use the
|
||||
marketplace while your account is suspended.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appeal Section -->
|
||||
<div class="appeal-section">
|
||||
<p class="appeal-text">
|
||||
If you believe this ban was made in error, you can submit an appeal.
|
||||
</p>
|
||||
<a href="/support" class="appeal-btn">
|
||||
<Mail :size="20" />
|
||||
<span>Contact Support</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<button @click="handleLogout" class="logout-btn">
|
||||
<LogOut :size="20" />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<div class="banned-footer">
|
||||
<p>For more information, please review our</p>
|
||||
<div class="footer-links">
|
||||
<a href="/terms" class="footer-link">Terms of Service</a>
|
||||
<span class="separator">•</span>
|
||||
<a href="/privacy" class="footer-link">Privacy Policy</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="social-links">
|
||||
<a
|
||||
:href="socialLinks.twitter"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="social-link"
|
||||
aria-label="X (Twitter)"
|
||||
>
|
||||
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
:href="socialLinks.discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="social-link"
|
||||
aria-label="Discord"
|
||||
>
|
||||
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 00-4.885-1.515.074.074 0 00-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 00-5.487 0 12.64 12.64 0 00-.617-1.25.077.077 0 00-.079-.037A19.736 19.736 0 003.677 4.37a.07.07 0 00-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 00.031.057 19.9 19.9 0 005.993 3.03.078.078 0 00.084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 00-.041-.106 13.107 13.107 0 01-1.872-.892.077.077 0 01-.008-.128 10.2 10.2 0 00.372-.292.074.074 0 01.077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 01.078.01c.12.098.246.198.373.292a.077.077 0 01-.006.127 12.299 12.299 0 01-1.873.892.077.077 0 00-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 00.084.028 19.839 19.839 0 006.002-3.03.077.077 0 00.032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 00-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { ShieldAlert, AlertCircle, Info, Mail, LogOut } from "lucide-vue-next";
|
||||
import axios from "@/utils/axios";
|
||||
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const socialLinks = ref({
|
||||
twitter: "https://x.com",
|
||||
discord: "https://discord.gg",
|
||||
});
|
||||
|
||||
const banInfo = computed(() => {
|
||||
if (!authStore.user) return null;
|
||||
|
||||
return {
|
||||
reason: authStore.user.ban?.reason,
|
||||
bannedAt: authStore.user.ban?.bannedAt,
|
||||
bannedUntil: authStore.user.ban?.bannedUntil,
|
||||
isPermanent:
|
||||
authStore.user.ban?.permanent || !authStore.user.ban?.bannedUntil,
|
||||
};
|
||||
});
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return "";
|
||||
|
||||
const d = new Date(date);
|
||||
return d.toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authStore.logout();
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
const fetchSocialLinks = async () => {
|
||||
try {
|
||||
const response = await axios.get("/api/config/public");
|
||||
if (response.data.success && response.data.config.social) {
|
||||
const social = response.data.config.social;
|
||||
if (social.twitter) {
|
||||
socialLinks.value.twitter = social.twitter;
|
||||
}
|
||||
if (social.discord) {
|
||||
socialLinks.value.discord = social.discord;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch social links:", error);
|
||||
// Keep default values if fetch fails
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// If user is not banned, redirect to home
|
||||
if (!authStore.isBanned) {
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
// Fetch social links
|
||||
fetchSocialLinks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banned-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banned-page::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(
|
||||
circle at 20% 50%,
|
||||
rgba(239, 68, 68, 0.1) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 80%,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
animation: pulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.banned-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background: rgba(30, 30, 46, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 1.5rem;
|
||||
padding: 3rem 2rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
margin-bottom: 2rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.banned-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #ef4444;
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.banned-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.banned-message {
|
||||
font-size: 1.125rem;
|
||||
color: #d1d5db;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.ban-details {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.9375rem;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.permanent-ban {
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
border-radius: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
flex-shrink: 0;
|
||||
color: #3b82f6;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 0.875rem;
|
||||
color: #d1d5db;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.appeal-section {
|
||||
margin: 2rem 0;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.appeal-text {
|
||||
font-size: 0.9375rem;
|
||||
color: #d1d5db;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.appeal-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem 2rem;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
border: 1px solid rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.appeal-btn:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.appeal-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #d1d5db;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.banned-footer {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.banned-footer p {
|
||||
color: #9ca3af;
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #60a5fa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.social-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #9ca3af;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.social-link:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.banned-container {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.banned-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.banned-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.appeal-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user