feat: add some more admin stuff

This commit is contained in:
Alexander Medvedev 2026-06-11 20:37:59 +02:00
parent f0e61ec9f8
commit 35e5a8d56b
5 changed files with 435 additions and 34 deletions

View File

@ -20,6 +20,7 @@ import ManagePlugin from './pages/dashboard/plugin/ManagePlugin';
import AddPlugin from './pages/dashboard/plugin/AddPlugin'; import AddPlugin from './pages/dashboard/plugin/AddPlugin';
import TermsOfServicePage from './pages/legal/TermsOfServicePage'; import TermsOfServicePage from './pages/legal/TermsOfServicePage';
import PrivacyPolicyPage from './pages/legal/PrivacyPolicyPage'; import PrivacyPolicyPage from './pages/legal/PrivacyPolicyPage';
import GuidelinesPage from './pages/legal/GuidelinesPage';
import AdminPanel from './pages/admin/AdminPanel'; import AdminPanel from './pages/admin/AdminPanel';
import './App.css'; import './App.css';
@ -131,6 +132,7 @@ const App = () => (
<Route path="search" element={<SearchResultsPage />} /> <Route path="search" element={<SearchResultsPage />} />
<Route path="terms" element={<TermsOfServicePage />} /> <Route path="terms" element={<TermsOfServicePage />} />
<Route path="privacy" element={<PrivacyPolicyPage />} /> <Route path="privacy" element={<PrivacyPolicyPage />} />
<Route path="guidelines" element={<GuidelinesPage />} />
<Route path="login" element={<LoginPage />} /> <Route path="login" element={<LoginPage />} />
<Route path="register" element={<RegisterPage />} /> <Route path="register" element={<RegisterPage />} />
<Route path="forgot-password" element={<ForgotPasswordPage />} /> <Route path="forgot-password" element={<ForgotPasswordPage />} />
@ -305,6 +307,15 @@ const Footer = () => (
> >
Privacy Privacy
</Link> </Link>
<Link
to="/guidelines"
style={{ fontSize: '12px', color: 'inherit', opacity: 0.4, textDecoration: 'none' }}
onMouseEnter={e => (e.currentTarget.style.opacity = '0.9')}
onMouseLeave={e => (e.currentTarget.style.opacity = '0.4')}
>
Guidelines
</Link>
</div> </div>
</footer> </footer>
); );

View File

@ -96,6 +96,49 @@
.role-badge.moderator { background: rgba(255, 209, 102, 0.15); color: #ffd166; border: 1px solid rgba(255, 209, 102, 0.3); } .role-badge.moderator { background: rgba(255, 209, 102, 0.15); color: #ffd166; border: 1px solid rgba(255, 209, 102, 0.3); }
.role-badge.user { background: rgba(79, 126, 255, 0.1); color: #4f7eff; border: 1px solid rgba(79, 126, 255, 0.2); } .role-badge.user { background: rgba(79, 126, 255, 0.1); color: #4f7eff; border: 1px solid rgba(79, 126, 255, 0.2); }
select.role-badge {
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
text-align: center;
}
select.role-badge option {
background: #1a1a1a;
color: #fff;
}
.row-deleted {
opacity: 0.6;
background: rgba(255, 255, 255, 0.02);
}
.status-active {
color: #4caf50;
font-weight: 600;
font-size: 0.85rem;
}
.status-deleted {
color: #ff6b6b;
font-weight: 600;
font-size: 0.85rem;
}
.admin-table td strong {
color: #ff6b6b;
}
.admin-table td {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
}
.type-badge.paid { background: rgba(255, 209, 102, 0.15); color: #ffd166; border: 1px solid rgba(255, 209, 102, 0.3); } .type-badge.paid { background: rgba(255, 209, 102, 0.15); color: #ffd166; border: 1px solid rgba(255, 209, 102, 0.3); }
.type-badge.free { background: rgba(6, 214, 160, 0.15); color: #06d6a0; border: 1px solid rgba(6, 214, 160, 0.3); } .type-badge.free { background: rgba(6, 214, 160, 0.15); color: #06d6a0; border: 1px solid rgba(6, 214, 160, 0.3); }
.type-badge.adwall { background: rgba(168, 85, 247, 0.15); color: #a855f7; border: 1px solid rgba(168, 85, 247, 0.3); } .type-badge.adwall { background: rgba(168, 85, 247, 0.15); color: #a855f7; border: 1px solid rgba(168, 85, 247, 0.3); }

View File

@ -3,9 +3,10 @@ import api from '../../api';
import './AdminPanel.css'; import './AdminPanel.css';
const AdminPanel = () => { const AdminPanel = () => {
const [activeTab, setActiveTab] = useState<'users' | 'plugins'>('users'); const [activeTab, setActiveTab] = useState<'users' | 'plugins' | 'logs'>('users');
const [users, setUsers] = useState<any[]>([]); const [users, setUsers] = useState<any[]>([]);
const [plugins, setPlugins] = useState<any[]>([]); const [plugins, setPlugins] = useState<any[]>([]);
const [logs, setLogs] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -20,34 +21,52 @@ const AdminPanel = () => {
if (activeTab === 'users') { if (activeTab === 'users') {
const res = await api.get('/admin/users'); const res = await api.get('/admin/users');
setUsers(Array.isArray(res.data) ? res.data : []); setUsers(Array.isArray(res.data) ? res.data : []);
} else { } else if (activeTab === 'plugins') {
const res = await api.get('/admin/plugins'); const res = await api.get('/admin/plugins');
setPlugins(Array.isArray(res.data) ? res.data : []); setPlugins(Array.isArray(res.data) ? res.data : []);
} else if (activeTab === 'logs') {
const res = await api.get('/admin/logs');
setLogs(Array.isArray(res.data) ? res.data : []);
} }
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || 'Failed to fetch data'); setError(err.response?.data?.error || 'Failed to fetch data');
setUsers([]); setUsers([]);
setPlugins([]); setPlugins([]);
setLogs([]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const deleteUser = async (id: number) => { const deleteUser = async (id: number) => {
if (!window.confirm('Are you sure you want to delete this user and all their plugins?')) return; const reason = window.prompt('Enter reason for deactivation:');
if (!reason) return;
try { try {
await api.delete(`/admin/users/${id}`); await api.delete(`/admin/users/${id}`, { data: { reason } });
setUsers(users.filter(u => u.id !== id)); setUsers(users.map(u => u.id === id ? { ...u, deleted_at: new Date().toISOString() } : u));
} catch (err: any) { } catch (err: any) {
alert(err.response?.data?.error || 'Failed to delete user'); alert(err.response?.data?.error || 'Failed to delete user');
} }
}; };
const deletePlugin = async (id: number) => { const changeRole = async (id: number, newRole: string) => {
if (!window.confirm('Are you sure you want to delete this plugin?')) return; const reason = window.prompt(`Enter reason for changing role to ${newRole}:`);
if (!reason) return;
try { try {
await api.delete(`/admin/plugins/${id}`); await api.put(`/admin/users/${id}/role`, { role: newRole, reason });
setPlugins(plugins.filter(p => p.id !== id)); setUsers(users.map(u => u.id === id ? { ...u, role: newRole } : u));
alert('Role updated successfully');
} catch (err: any) {
alert(err.response?.data?.error || 'Failed to update role');
}
};
const deletePlugin = async (id: number) => {
const reason = window.prompt('Enter reason for removal:');
if (!reason) return;
try {
await api.delete(`/admin/plugins/${id}`, { data: { reason } });
setPlugins(plugins.map(p => p.id === id ? { ...p, deleted_at: new Date().toISOString() } : p));
} catch (err: any) { } catch (err: any) {
alert(err.response?.data?.error || 'Failed to delete plugin'); alert(err.response?.data?.error || 'Failed to delete plugin');
} }
@ -70,6 +89,12 @@ const AdminPanel = () => {
> >
Plugins Plugins
</button> </button>
<button
className={activeTab === 'logs' ? 'active' : ''}
onClick={() => setActiveTab('logs')}
>
Logs
</button>
</div> </div>
</header> </header>
@ -87,21 +112,61 @@ const AdminPanel = () => {
<th>Email</th> <th>Email</th>
<th>Role</th> <th>Role</th>
<th>Stripe</th> <th>Stripe</th>
<th>Created</th> <th>Status</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map(u => ( {users.map(u => (
<tr key={u.id}> <tr key={u.id} className={u.deleted_at ? 'row-deleted' : ''}>
<td data-label="ID">{u.id}</td> <td data-label="ID">{u.id}</td>
<td data-label="Username">{u.username}</td> <td data-label="Username">{u.username}</td>
<td data-label="Email">{u.email}</td> <td data-label="Email">{u.email}</td>
<td data-label="Role"><span className={`role-badge ${u.role}`}>{u.role}</span></td> <td data-label="Role">
<select
className={`role-badge ${u.role}`}
value={u.role}
onChange={(e) => changeRole(u.id, e.target.value)}
disabled={!!u.deleted_at}
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
</td>
<td data-label="Stripe">{u.stripe_ready ? '✅' : '❌'}</td> <td data-label="Stripe">{u.stripe_ready ? '✅' : '❌'}</td>
<td data-label="Created">{u.created_at ? new Date(u.created_at).toLocaleDateString() : 'N/A'}</td> <td data-label="Status">{u.deleted_at ? <span className="status-deleted">Deleted</span> : <span className="status-active">Active</span>}</td>
<td data-label="Actions"> <td data-label="Actions">
<button className="btn-delete" onClick={() => deleteUser(u.id)}>Delete</button> {!u.deleted_at && <button className="btn-delete" onClick={() => deleteUser(u.id)}>Delete</button>}
</td>
</tr>
))}
</tbody>
</table>
) : activeTab === 'plugins' ? (
<table className="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Type</th>
<th>Price</th>
<th>Developer</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{plugins.map(p => (
<tr key={p.id} className={p.deleted_at ? 'row-deleted' : ''}>
<td data-label="ID">{p.id}</td>
<td data-label="Name">{p.name}</td>
<td data-label="Type"><span className={`type-badge ${p.type}`}>{p.type}</span></td>
<td data-label="Price">€{(p.price_cents / 100).toFixed(2)}</td>
<td data-label="Developer">{p.dev_name}</td>
<td data-label="Status">{p.deleted_at ? <span className="status-deleted">Deleted</span> : <span className="status-active">Active</span>}</td>
<td data-label="Actions">
{!p.deleted_at && <button className="btn-delete" onClick={() => deletePlugin(p.id)}>Delete</button>}
</td> </td>
</tr> </tr>
))} ))}
@ -111,29 +176,21 @@ const AdminPanel = () => {
<table className="admin-table"> <table className="admin-table">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>Date</th>
<th>Name</th> <th>Moderator</th>
<th>Type</th> <th>Action</th>
<th>Price</th> <th>Target</th>
<th>Developer</th> <th>Reason</th>
<th>Downloads</th>
<th>Views</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{plugins.map(p => ( {logs.map(l => (
<tr key={p.id}> <tr key={l.id}>
<td data-label="ID">{p.id}</td> <td data-label="Date">{new Date(l.created_at).toLocaleString()}</td>
<td data-label="Name">{p.name}</td> <td data-label="Moderator">{l.moderator_name}</td>
<td data-label="Type"><span className={`type-badge ${p.type}`}>{p.type}</span></td> <td data-label="Action"><strong>{l.action.toUpperCase()}</strong></td>
<td data-label="Price">€{(p.price_cents / 100).toFixed(2)}</td> <td data-label="Target">{l.target_type} ({l.target_id})</td>
<td data-label="Developer">{p.dev_name}</td> <td data-label="Reason">{l.reason || 'No reason provided'}</td>
<td data-label="Downloads">{p.downloads}</td>
<td data-label="Views">{p.views}</td>
<td data-label="Actions">
<button className="btn-delete" onClick={() => deletePlugin(p.id)}>Delete</button>
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@ -0,0 +1,171 @@
import React from 'react';
import './LegalPages.css';
const GuidelinesPage = () => {
return (
<div className="legal-container guidelines-page">
<div className="legal-content">
<header className="guidelines-header">
<h1>PumpkinMarket <span>Review Guidelines</span></h1>
<p className="subtitle">High-quality plugins for a high-quality community.</p>
<div className="last-updated">Last Updated: June 11, 2026</div>
</header>
<section className="introduction">
<p>
Plugins are a great way to extend the functionality of PumpkinMC servers. Our goal is to provide a safe experience for users
and a great opportunity for every developer to succeed. We’ve developed these Review Guidelines to help you through
the development process.
</p>
<div className="callout">
<h3>Before You Submit</h3>
<ul>
<li>Ensure your plugin is complete and functional.</li>
<li>Test for crashes and server performance impact.</li>
<li>Check that all links, icons, and metadata are accurate and final.</li>
<li>Verify that your plugin follows all security best practices.</li>
</ul>
</div>
</section>
<section className="guideline-section">
<h2>1. Safety</h2>
<p className="section-intro">When users download a plugin from PumpkinMarket, they need to be certain that it is safe to do so. Your plugin should not contain self-replicating code, backdoors, or any features that could harm a server or its data.</p>
<div className="sub-section">
<h3>1.1 Objectionable Content</h3>
<p>Plugins should not include content that is offensive, insensitive, upsetting, intended to disgust, or in exceptionally poor taste. Examples of such content include:</p>
<ul>
<li><strong>1.1.1 Defamatory or Discriminatory:</strong> Content that is defamatory, discriminatory, or mean-spirited, including references or commentary about religion, race, sexual orientation, gender, national/ethnic origin, or other targeted groups.</li>
<li><strong>1.1.2 Violence:</strong> Realistic depictions of physical harm or violence against people or animals. Content that encourages self-harm or illegal acts.</li>
<li><strong>1.1.3 Sexual Content:</strong> Overtly sexual or pornographic material, including "adult-themed" plugins.</li>
<li><strong>1.1.4 Harassment:</strong> Features designed to facilitate doxxing, stalking, or targeted harassment of server players.</li>
</ul>
</div>
<div className="sub-section">
<h3>1.2 User Protection</h3>
<p>PumpkinMarket is dedicated to the prevention of "griefing" and malicious exploitation at the platform level.</p>
<ul>
<li><strong>1.2.1 Malware:</strong> Plugins must not contain any malicious code. This includes viruses, backdoors, hidden "OP" commands, or any code that performs actions without the server owner's knowledge.</li>
<li><strong>1.2.2 Phishing:</strong> Plugins must not attempt to steal user credentials, tokens (like Discord tokens), or any sensitive configuration files from the server host.</li>
<li><strong>1.2.3 Remote Execution:</strong> Plugins must not download and execute arbitrary code from external sources that was not included in the original WASM binary approved by the market.</li>
</ul>
</div>
<div className="sub-section">
<h3>1.3 Data Privacy</h3>
<p>Data privacy is a fundamental right. Plugins must be transparent about data collection.</p>
<ul>
<li><strong>1.3.1 Transparency:</strong> If your plugin sends data to an external server (e.g., for analytics or licensing), you must disclose this clearly in the description.</li>
<li><strong>1.3.2 Opt-out:</strong> All telemetry or external reporting must provide a configuration option to be disabled.</li>
<li><strong>1.3.3 Sensitive Files:</strong> Plugins must not access or upload files outside of their designated data folder without clear justification and disclosure.</li>
</ul>
</div>
</section>
<section className="guideline-section">
<h2>2. Performance</h2>
<p className="section-intro">Users expect plugins to be reliable and efficient. Unoptimized plugins can crash servers or cause significant lag, leading to a poor user experience.</p>
<div className="sub-section">
<h3>2.1 Plugin Completeness</h3>
<p>PumpkinMarket will reject plugins that are not yet complete. We do not accept "placeholder" plugins or versions that are non-functional.</p>
<ul>
<li><strong>2.1.1 Minimum Functionality:</strong> Plugins must provide a meaningful extension to the server experience. "Empty" plugins or those that only print a message to the console will be rejected.</li>
<li><strong>2.1.2 Stability:</strong> Plugins that cause the server to crash or hang during normal operation will be removed.</li>
</ul>
</div>
<div className="sub-section">
<h3>2.2 Accurate Metadata</h3>
<p>The information you provide about your plugin must be accurate and honest.</p>
<ul>
<li><strong>2.2.1 Screenshots:</strong> Screenshots must accurately reflect the plugin's interface or effects in-game. Do not use generic or "stock" images that are unrelated.</li>
<li><strong>2.2.2 Categorization:</strong> Plugins must be placed in the most relevant category. Mis-categorizing to gain visibility is a violation.</li>
<li><strong>2.2.3 Descriptions:</strong> Do not include irrelevant keywords or "tag spam" in your description to manipulate search results.</li>
</ul>
</div>
<div className="sub-section">
<h3>2.3 Resource Management</h3>
<p>PumpkinMC servers run in high-concurrency environments. Performance is critical.</p>
<ul>
<li><strong>2.3.1 CPU usage:</strong> Avoid blocking the main server thread. Use asynchronous tasks where possible.</li>
<li><strong>2.3.2 Memory leaks:</strong> Ensure your WASM binary does not leak memory over long runtimes.</li>
</ul>
</div>
</section>
<section className="guideline-section">
<h2>3. Business</h2>
<p className="section-intro">monetization on PumpkinMarket should be transparent and provide clear value to the user.</p>
<div className="sub-section">
<h3>3.1 Payments</h3>
<ul>
<li><strong>3.1.1 Integrated Payments:</strong> All transactions for plugins must go through the PumpkinMarket system. Linking to Patreon, PayPal, or other external payment sites within the plugin or description is not allowed.</li>
<li><strong>3.1.2 Digital Goods:</strong> Plugins must not unlock features via external "keys" or "codes" purchased outside the market.</li>
</ul>
</div>
<div className="sub-section">
<h3>3.2 Business Practices</h3>
<ul>
<li><strong>3.2.1 Bait and Switch:</strong> Plugins must not change their primary purpose after approval or hide essential features behind unexpected paywalls.</li>
<li><strong>3.2.2 Fake Reviews:</strong> Developers found to be inflating their own ratings or reviews will be permanently banned.</li>
<li><strong>3.2.3 Scamming:</strong> Any plugin found to be a scam or "vaporware" will result in an immediate permanent ban and possible refunding of all affected users.</li>
</ul>
</div>
</section>
<section className="guideline-section">
<h2>4. Design</h2>
<p className="section-intro">A great plugin is more than just code; it's also about how it's presented and how users interact with it.</p>
<div className="sub-section">
<h3>4.1 Copycats and Spam</h3>
<ul>
<li><strong>4.1.1 Originality:</strong> Do not simply "clone" another popular plugin. While similar features are allowed, your plugin should offer unique value or a different implementation.</li>
<li><strong>4.1.2 Spam:</strong> Do not upload multiple near-identical plugins. Consolidate features into a single, high-quality plugin with configuration options.</li>
</ul>
</div>
<div className="sub-section">
<h3>4.2 User Experience</h3>
<ul>
<li><strong>4.2.1 Consistency:</strong> We recommend following standard command patterns (e.g., /pluginname help) to ensure a familiar experience for users.</li>
<li><strong>4.2.2 Documentation:</strong> Provide clear instructions in your description or a linked wiki. Plugins that are impossible to configure or use will be rejected.</li>
</ul>
</div>
</section>
<section className="guideline-section">
<h2>5. Legal</h2>
<p className="section-intro">You are responsible for ensuring your plugin complies with all applicable laws and regulations.</p>
<div className="sub-section">
<h3>5.1 Intellectual Property</h3>
<p>You must be the original author of the code or have the legal right to distribute it. Using leaked or stolen code ("leaks") will result in an immediate ban.</p>
</div>
<div className="sub-section">
<h3>5.2 Open Source Licenses</h3>
<p>If your plugin includes open-source components, you must adhere to the terms of those licenses. Include necessary attributions and license files within your plugin archive.</p>
</div>
<div className="sub-section">
<h3>5.3 Right to Remove</h3>
<p>PumpkinMarket reserves the right to remove any plugin at its sole discretion if it is determined to be detrimental to the market's reputation or user safety, even if a specific guideline has not been technically violated.</p>
</div>
</section>
<footer className="guidelines-footer">
<p>We hope these guidelines help you build better plugins and a better PumpkinMarket. We’re excited to see what you create!</p>
</footer>
</div>
</div>
);
};
export default GuidelinesPage;

View File

@ -0,0 +1,119 @@
.legal-container {
max-width: 900px;
margin: 0 auto;
padding: 80px 24px;
color: #e0e0e0;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.legal-content h1 {
font-size: 3rem;
font-weight: 800;
margin-bottom: 0.5rem;
color: #fff;
letter-spacing: -0.02em;
}
.legal-content h1 span {
color: #ffb74d;
}
.subtitle {
font-size: 1.25rem;
color: #999;
margin-bottom: 2rem;
}
.last-updated {
font-size: 0.9rem;
color: #666;
margin-bottom: 3rem;
border-bottom: 1px solid #333;
padding-bottom: 2rem;
}
.introduction p {
font-size: 1.15rem;
margin-bottom: 2rem;
}
.callout {
background: rgba(255, 183, 77, 0.05);
border: 1px solid rgba(255, 183, 77, 0.2);
border-radius: 12px;
padding: 24px;
margin: 2rem 0;
}
.callout h3 {
margin-top: 0;
color: #ffb74d;
font-size: 1.1rem;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}
.callout ul {
margin: 0;
padding-left: 20px;
}
.callout li {
margin-bottom: 8px;
color: #ccc;
}
.guideline-section {
margin-top: 4rem;
}
.guideline-section h2 {
font-size: 2rem;
color: #fff;
margin-bottom: 1rem;
border-bottom: 2px solid #333;
padding-bottom: 0.5rem;
}
.section-intro {
font-size: 1.1rem;
color: #bbb;
margin-bottom: 2rem;
}
.sub-section {
margin-bottom: 2rem;
padding-left: 20px;
border-left: 2px solid #222;
}
.sub-section h3 {
font-size: 1.25rem;
color: #ffb74d;
margin-bottom: 0.5rem;
}
.sub-section p {
color: #aaa;
margin: 0;
}
.guidelines-footer {
margin-top: 6rem;
padding-top: 3rem;
border-top: 1px solid #333;
text-align: center;
color: #666;
}
@media (max-width: 768px) {
.legal-content h1 {
font-size: 2.25rem;
}
.legal-container {
padding: 40px 20px;
}
}