mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 20:55:12 +00:00
add creator role
This commit is contained in:
parent
f97e52b0a0
commit
a79c27c838
2 changed files with 183 additions and 10 deletions
|
|
@ -2,6 +2,8 @@ import { Elysia } from "elysia";
|
|||
import { eq, ne, and, inArray, sql, desc, asc, or, isNull } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import { usersTable, userBonusesTable } from "../schemas/users";
|
||||
import { sessionsTable } from "../schemas/sessions";
|
||||
import { userActivityTable } from "../schemas/user-emails";
|
||||
import { projectsTable } from "../schemas/projects";
|
||||
import { reviewsTable } from "../schemas/reviews";
|
||||
import {
|
||||
|
|
@ -42,7 +44,7 @@ const admin = new Elysia({ prefix: "/admin" });
|
|||
async function requireReviewer(headers: Record<string, string>) {
|
||||
const user = await getUserFromSession(headers);
|
||||
if (!user) return null;
|
||||
if (user.role !== "reviewer" && user.role !== "admin") return null;
|
||||
if (user.role !== "reviewer" && user.role !== "admin" && user.role !== "creator") return null;
|
||||
return user;
|
||||
}
|
||||
|
||||
|
|
@ -53,6 +55,13 @@ async function requireAdmin(headers: Record<string, string>) {
|
|||
return user;
|
||||
}
|
||||
|
||||
async function requireCreator(headers: Record<string, string>) {
|
||||
const user = await getUserFromSession(headers);
|
||||
if (!user) return null;
|
||||
if (user.role !== "creator") return null;
|
||||
return user;
|
||||
}
|
||||
|
||||
// Get admin stats (info page)
|
||||
admin.get("/stats", async ({ headers, status }) => {
|
||||
const user = await requireReviewer(headers as Record<string, string>);
|
||||
|
|
@ -548,7 +557,7 @@ admin.put("/users/:id/role", async ({ params, body, headers, status }) => {
|
|||
}
|
||||
|
||||
const { role } = body as { role: string };
|
||||
if (!["member", "reviewer", "admin", "banned"].includes(role)) {
|
||||
if (!["member", "reviewer", "admin", "creator", "banned"].includes(role)) {
|
||||
return status(400, { error: "Invalid role" });
|
||||
}
|
||||
|
||||
|
|
@ -3581,6 +3590,97 @@ admin.post("/recalculate-shop-pricing", async ({ headers, status }) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Delete user and all associated records (creator only)
|
||||
admin.delete("/users/:id", async ({ params, headers, status }) => {
|
||||
const user = await requireCreator(headers as Record<string, string>);
|
||||
if (!user) {
|
||||
return status(401, { error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const targetId = parseInt(params.id);
|
||||
if (isNaN(targetId)) {
|
||||
return status(400, { error: "Invalid user ID" });
|
||||
}
|
||||
|
||||
if (user.id === targetId) {
|
||||
return status(400, { error: "Cannot delete yourself" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all project IDs for the user before deleting anything
|
||||
const userProjects = await db
|
||||
.select({ id: projectsTable.id })
|
||||
.from(projectsTable)
|
||||
.where(eq(projectsTable.userId, targetId));
|
||||
const projectIds = userProjects.map((p) => p.id);
|
||||
|
||||
// 1. Delete sessions
|
||||
await db.delete(sessionsTable).where(eq(sessionsTable.userId, targetId));
|
||||
|
||||
// 2. Delete user activity
|
||||
await db.delete(userActivityTable).where(eq(userActivityTable.userId, targetId));
|
||||
|
||||
// 3. Delete shop hearts
|
||||
await db.delete(shopHeartsTable).where(eq(shopHeartsTable.userId, targetId));
|
||||
|
||||
// 4. Delete shop penalties
|
||||
await db.delete(shopPenaltiesTable).where(eq(shopPenaltiesTable.userId, targetId));
|
||||
|
||||
// 5. Delete shop rolls
|
||||
await db.delete(shopRollsTable).where(eq(shopRollsTable.userId, targetId));
|
||||
|
||||
// 6. Delete refinery spending history
|
||||
await db.delete(refinerySpendingHistoryTable).where(eq(refinerySpendingHistoryTable.userId, targetId));
|
||||
|
||||
// 7. Delete refinery orders
|
||||
await db.delete(refineryOrdersTable).where(eq(refineryOrdersTable.userId, targetId));
|
||||
|
||||
// 8. Delete shop orders
|
||||
await db.delete(shopOrdersTable).where(eq(shopOrdersTable.userId, targetId));
|
||||
|
||||
// 9. Null out givenBy on bonuses given by this user (so other users' bonuses remain)
|
||||
await db
|
||||
.update(userBonusesTable)
|
||||
.set({ givenBy: null })
|
||||
.where(eq(userBonusesTable.givenBy, targetId));
|
||||
|
||||
// 10. Delete user's own bonuses
|
||||
await db.delete(userBonusesTable).where(eq(userBonusesTable.userId, targetId));
|
||||
|
||||
// 11. Delete reviews done by this user as reviewer
|
||||
await db.delete(reviewsTable).where(eq(reviewsTable.reviewerId, targetId));
|
||||
|
||||
if (projectIds.length > 0) {
|
||||
// 12. Delete project activity for user's projects (from any user)
|
||||
await db.delete(projectActivityTable).where(inArray(projectActivityTable.projectId, projectIds));
|
||||
|
||||
// 13. Delete reviews on user's projects
|
||||
await db.delete(reviewsTable).where(inArray(reviewsTable.projectId, projectIds));
|
||||
}
|
||||
|
||||
// 14. Delete project activity by this user (on any project)
|
||||
await db.delete(projectActivityTable).where(eq(projectActivityTable.userId, targetId));
|
||||
|
||||
// 15. Delete user's projects
|
||||
await db.delete(projectsTable).where(eq(projectsTable.userId, targetId));
|
||||
|
||||
// 16. Delete the user
|
||||
const deleted = await db
|
||||
.delete(usersTable)
|
||||
.where(eq(usersTable.id, targetId))
|
||||
.returning();
|
||||
|
||||
if (!deleted[0]) {
|
||||
return status(404, { error: "User not found" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error("[ADMIN] Delete user error:", err);
|
||||
return status(500, { error: "Failed to delete user" });
|
||||
}
|
||||
});
|
||||
|
||||
admin.get("/config", async () => {
|
||||
// Public endpoint: returns canonical pricing constants for the frontend to consume.
|
||||
// These are intentionally read from the server-side constants so UI and server stay in sync.
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@
|
|||
let unshipReason = $state('');
|
||||
let unshipping = $state(false);
|
||||
|
||||
let showDeleteUserConfirm = $state(false);
|
||||
let deletingUser = $state(false);
|
||||
|
||||
// lightweight toast helper (DOM-based, so no extra markup required)
|
||||
function _showToast(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
||||
try {
|
||||
|
|
@ -166,7 +169,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
currentUser = await getUser();
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'reviewer')) {
|
||||
if (!currentUser || (currentUser.role !== 'admin' && currentUser.role !== 'reviewer' && currentUser.role !== 'creator')) {
|
||||
goto('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
|
@ -372,6 +375,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteUser() {
|
||||
if (!targetUser) return;
|
||||
deletingUser = true;
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/admin/users/${targetUser.id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
_showToast(result.error || 'Failed to delete user', 'error');
|
||||
return;
|
||||
}
|
||||
_showToast('User deleted successfully', 'success');
|
||||
goto('/admin/users');
|
||||
} catch (e) {
|
||||
console.error('Failed to delete user:', e);
|
||||
_showToast('Failed to delete user', 'error');
|
||||
} finally {
|
||||
deletingUser = false;
|
||||
showDeleteUserConfirm = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTimelineIcon(type: string) {
|
||||
switch (type) {
|
||||
case 'earned':
|
||||
|
|
@ -425,6 +452,8 @@
|
|||
switch (role) {
|
||||
case 'admin':
|
||||
return 'bg-red-100 text-red-700';
|
||||
case 'creator':
|
||||
return 'bg-purple-100 text-purple-700';
|
||||
case 'reviewer':
|
||||
return 'bg-blue-100 text-blue-700';
|
||||
case 'banned':
|
||||
|
|
@ -654,13 +683,24 @@
|
|||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={saveChanges}
|
||||
disabled={saving}
|
||||
class="cursor-pointer rounded-full bg-black px-6 py-2 font-bold text-white transition-all hover:bg-gray-800 disabled:opacity-50"
|
||||
>
|
||||
{saving ? $t.common.saving : $t.project.saveChanges}
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
onclick={saveChanges}
|
||||
disabled={saving}
|
||||
class="cursor-pointer rounded-full bg-black px-6 py-2 font-bold text-white transition-all hover:bg-gray-800 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? $t.common.saving : $t.project.saveChanges}
|
||||
</button>
|
||||
{#if currentUser?.role === 'creator'}
|
||||
<button
|
||||
onclick={() => (showDeleteUserConfirm = true)}
|
||||
disabled={deletingUser}
|
||||
class="cursor-pointer rounded-full border-4 border-red-600 px-6 py-2 font-bold text-red-600 transition-all duration-200 hover:border-dashed disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
delete user
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1067,3 +1107,36 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showDeleteUserConfirm}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onclick={(e) => e.target === e.currentTarget && (showDeleteUserConfirm = false)}
|
||||
onkeydown={(e) => e.key === 'Escape' && (showDeleteUserConfirm = false)}
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="w-full max-w-md rounded-2xl border-4 border-black bg-white p-6">
|
||||
<h2 class="mb-4 text-2xl font-bold">delete user</h2>
|
||||
<p class="mb-6 text-gray-600">
|
||||
are you sure you want to permanently delete <strong>{targetUser?.username || 'this user'}</strong>?
|
||||
this will remove all their data including projects, orders, bonuses, and sessions. this cannot be undone.
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
onclick={() => (showDeleteUserConfirm = false)}
|
||||
class="flex-1 cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={deleteUser}
|
||||
disabled={deletingUser}
|
||||
class="flex-1 cursor-pointer rounded-full border-4 border-red-600 bg-red-600 px-4 py-2 font-bold text-white transition-all duration-200 hover:border-dashed disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{deletingUser ? 'deleting...' : 'delete permanently'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue