revert fb8e77bb68
Local Build and Deploy / deploy (push) Successful in 1m11s

revert style: clean up code with prettier and eslint
This commit is contained in:
remco committed 2026-07-12 21:02:03 +02:00
1 parent 27de078f54
commit e85e4d74ea
378 files changed
+20710 -22428

No files matched your search

+1 -7
View File
@@ -19,13 +19,7 @@ export async function updateBadge({ code, name, desc }: { code: string; name: st
await prisma.websiteBadges.upsert({
where: { badgeKey: code },
update: { badgeName: name, badgeDescription: desc, updatedAt: new Date() },
create: {
badgeKey: code,
badgeName: name,
badgeDescription: desc,
createdAt: new Date(),
updatedAt: new Date(),
},
create: { badgeKey: code, badgeName: name, badgeDescription: desc, createdAt: new Date(), updatedAt: new Date() },
});
revalidatePath("/admin/import/badges");
}
+34 -34
View File
@@ -1,70 +1,70 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
const bannerSchema = z.object({
title: z.string().min(1).max(255),
subtitle: z.string().max(500).optional().default(""),
subtitle: z.string().max(500).optional().default(''),
image: z.string().max(500),
link: z.string().max(500).optional().default(""),
color: z.string().max(20).optional().default(""),
link: z.string().max(500).optional().default(''),
color: z.string().max(20).optional().default(''),
isActive: z.coerce.number().int().min(0).max(1).default(1),
sortOrder: z.coerce.number().int().min(0).default(0),
startDate: z.string().max(50).nullable().optional(),
endDate: z.string().max(50).nullable().optional(),
});
})
export const createBanner = adminAction(
{ permission: PERMS.BANNERS_EDIT, schema: bannerSchema },
async (ctx) => {
const banner = await prisma.websiteBanner.create({ data: ctx.data });
const banner = await prisma.websiteBanner.create({ data: ctx.data })
logAudit({
userId: ctx.session.user.id,
action: "banner_create",
target: "WebsiteBanner",
action: 'banner_create',
target: 'WebsiteBanner',
targetId: banner.id,
after: { title: banner.title },
});
return actionOk({ id: banner.id });
})
return actionOk({ id: banner.id })
},
);
)
const updateBannerInput = bannerSchema.partial().extend({ id: z.coerce.number().int().positive() });
const updateBannerInput = bannerSchema.partial().extend({ id: z.coerce.number().int().positive() })
export const updateBanner = adminAction(
{ permission: PERMS.BANNERS_EDIT, schema: updateBannerInput },
async (ctx) => {
const { id, ...data } = ctx.data;
const existing = await prisma.websiteBanner.findUnique({ where: { id } });
if (!existing) throw new ActionError("Banner not found");
await prisma.websiteBanner.update({ where: { id }, data });
const { id, ...data } = ctx.data
const existing = await prisma.websiteBanner.findUnique({ where: { id } })
if (!existing) throw new ActionError('Banner not found')
await prisma.websiteBanner.update({ where: { id }, data })
logAudit({
userId: ctx.session.user.id,
action: "banner_update",
target: "WebsiteBanner",
action: 'banner_update',
target: 'WebsiteBanner',
targetId: id,
});
return actionOk({ id });
})
return actionOk({ id })
},
);
)
const deleteBannerInput = z.object({ id: z.coerce.number().int().positive() });
const deleteBannerInput = z.object({ id: z.coerce.number().int().positive() })
export const deleteBanner = adminAction(
{ permission: PERMS.BANNERS_EDIT, schema: deleteBannerInput },
async (ctx) => {
await prisma.websiteBanner.delete({ where: { id: ctx.data.id } });
await prisma.websiteBanner.delete({ where: { id: ctx.data.id } })
logAudit({
userId: ctx.session.user.id,
action: "banner_delete",
target: "WebsiteBanner",
action: 'banner_delete',
target: 'WebsiteBanner',
targetId: ctx.data.id,
});
return actionOk();
})
return actionOk()
},
);
)
+4 -34
View File
@@ -7,11 +7,7 @@ import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
import type { ActionResult } from "@/lib/safe-action-shared";
export async function bulkUnban({
userIds,
}: {
userIds: number[];
}): Promise<ActionResult<{ unbanned: number; total: number }>> {
export async function bulkUnban({ userIds }: { userIds: number[] }): Promise<ActionResult<{ unbanned: number; total: number }>> {
const staff = await requireStaff();
const result = await prisma.ban.deleteMany({ where: { userId: { in: userIds } } });
await logStaffActivity({
@@ -23,15 +19,7 @@ export async function bulkUnban({
return { ok: true as const, data: { unbanned: result.count, total: userIds.length } };
}
export async function bulkBan({
userIds,
reason,
duration,
}: {
userIds: number[];
reason: string;
duration: number;
}): Promise<ActionResult<{ banned: number }>> {
export async function bulkBan({ userIds, reason, duration }: { userIds: number[]; reason: string; duration: number }): Promise<ActionResult<{ banned: number }>> {
const staff = await requireStaff();
const now = Math.floor(Date.now() / 1000);
let banned = 0;
@@ -65,17 +53,7 @@ export async function bulkBan({
return { ok: true as const, data: { banned } };
}
export async function bulkGiveCurrency({
userIds,
amount,
type,
}: {
userIds: number[];
amount: number;
type: "credits" | "pixels" | "points";
}): Promise<
ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>
> {
export async function bulkGiveCurrency({ userIds, amount, type }: { userIds: number[]; amount: number; type: "credits" | "pixels" | "points" }): Promise<ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>> {
const staff = await requireStaff();
let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
@@ -115,15 +93,7 @@ export async function bulkGiveCurrency({
return { ok: true as const, data: { given, total: userIds.length, failedIds } };
}
export async function bulkGiveBadge({
userIds,
badgeCode,
}: {
userIds: number[];
badgeCode: string;
}): Promise<
ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>
> {
export async function bulkGiveBadge({ userIds, badgeCode }: { userIds: number[]; badgeCode: string }): Promise<ActionResult<{ given: number; total: number; failedIds: Array<{ userId: number; reason: string }> }>> {
const staff = await requireStaff();
let given = 0;
const failedIds: Array<{ userId: number; reason: string }> = [];
+3 -24
View File
@@ -33,16 +33,7 @@ export async function deleteBcItem({ id }: { id: number }) {
revalidatePath("/admin/catalog/builder-club");
}
export async function updateBcItem({
id,
...data
}: {
id: number;
itemIds?: string;
catalogName?: string;
orderNumber?: number;
extradata?: string;
}) {
export async function updateBcItem({ id, ...data }: { id: number; itemIds?: string; catalogName?: string; orderNumber?: number; extradata?: string }) {
const staff = await requireStaff();
await prisma.catalogItemsBc.update({ where: { id }, data: data as any });
await logStaffActivity({
@@ -55,16 +46,7 @@ export async function updateBcItem({
revalidatePath("/admin/catalog/builder-club");
}
export async function createBcItem({
pageId,
...data
}: {
pageId: number;
itemIds: string;
catalogName: string;
orderNumber: number;
extradata: string;
}) {
export async function createBcItem({ pageId, ...data }: { pageId: number; itemIds: string; catalogName: string; orderNumber: number; extradata: string }) {
const staff = await requireStaff();
const created = await prisma.catalogItemsBc.create({
data: { pageId, ...data },
@@ -81,10 +63,7 @@ export async function createBcItem({
export async function toggleBcPage({ id, field }: { id: number; field: "enabled" | "visible" }) {
await requireStaff();
const page = await prisma.catalogPagesBc.findUnique({
where: { id },
select: { enabled: true, visible: true },
});
const page = await prisma.catalogPagesBc.findUnique({ where: { id }, select: { enabled: true, visible: true } });
if (!page) return;
await prisma.catalogPagesBc.update({
where: { id },
+6 -34
View File
@@ -62,11 +62,7 @@ export async function moveCatalogItems({ ids, targetPageId }: { ids: number[]; t
return { ok: true as const, data: {} };
}
export async function reorderCatalogItems({
orders,
}: {
orders: Array<{ id: number; orderNumber: number }>;
}) {
export async function reorderCatalogItems({ orders }: { orders: Array<{ id: number; orderNumber: number }> }) {
await requireStaff();
for (const { id, orderNumber } of orders) {
await prisma.catalogItems.update({ where: { id }, data: { orderNumber } });
@@ -76,15 +72,7 @@ export async function reorderCatalogItems({
return { ok: true as const, data: {} };
}
export async function updateCatalogItem({
id,
catalogFields,
baseItem,
}: {
id: number;
catalogFields: Record<string, unknown>;
baseItem?: { id: number; fields: Record<string, unknown> };
}) {
export async function updateCatalogItem({ id, catalogFields, baseItem }: { id: number; catalogFields: Record<string, unknown>; baseItem?: { id: number; fields: Record<string, unknown> } }) {
const staff = await requireStaff();
await prisma.catalogItems.update({ where: { id }, data: catalogFields as any });
if (baseItem) {
@@ -102,22 +90,15 @@ export async function updateCatalogItem({
return { ok: true as const, data: {} };
}
export async function translateCatalogItems({
items,
}: {
items: Array<{ id: number; publicName: string; description: string }>;
}) {
export async function translateCatalogItems({ items }: { items: Array<{ id: number; publicName: string; description: string }> }) {
await requireStaff();
let namesUpdated = 0;
let descriptionsUpdated = 0;
let furniDataUpdated = 0;
const furniDataInserted = 0;
let furniDataInserted = 0;
for (const item of items) {
const existing = await prisma.catalogItems.findUnique({
where: { id: item.id },
select: { catalogName: true },
});
const existing = await prisma.catalogItems.findUnique({ where: { id: item.id }, select: { catalogName: true } });
if (!existing) continue;
if (item.publicName && item.publicName !== existing.catalogName) {
@@ -146,14 +127,5 @@ export async function translateCatalogItems({
await rcon.updateCatalog();
revalidatePath("/admin/catalog");
return {
ok: true as const,
data: {
namesUpdated,
descriptionsUpdated,
furniDataUpdated,
furniDataInserted: 0,
updated: items.length,
},
};
return { ok: true as const, data: { namesUpdated, descriptionsUpdated, furniDataUpdated, furniDataInserted: 0, updated: items.length } };
}
+9 -27
View File
@@ -7,10 +7,7 @@ import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
import type { ActionResult } from "@/lib/safe-action-shared";
export async function updateCatalogPage({
id,
...fields
}: { id: number } & Record<string, unknown>): Promise<ActionResult> {
export async function updateCatalogPage({ id, ...fields }: { id: number } & Record<string, unknown>): Promise<ActionResult> {
const staff = await requireStaff();
await prisma.catalogPages.update({ where: { id }, data: fields as any });
await rcon.updateCatalog();
@@ -40,18 +37,9 @@ export async function deleteCatalogPage({ id }: { id: number }) {
return { ok: true as const, data: {} };
}
export async function toggleCatalogPage({
id,
action,
}: {
id: number;
action: "toggleEnabled" | "toggleVisible";
}) {
export async function toggleCatalogPage({ id, action }: { id: number; action: "toggleEnabled" | "toggleVisible" }) {
await requireStaff();
const page = await prisma.catalogPages.findUnique({
where: { id },
select: { enabled: true, visible: true },
});
const page = await prisma.catalogPages.findUnique({ where: { id }, select: { enabled: true, visible: true } });
if (!page) return { ok: false as const, error: "Catalog page not found" };
const field = action === "toggleEnabled" ? "enabled" : "visible";
const current = action === "toggleEnabled" ? page.enabled : page.visible;
@@ -63,17 +51,7 @@ export async function toggleCatalogPage({
return { ok: true as const, data: {} };
}
export async function createCatalogPage(input: {
caption: string;
parentId: number;
pageLayout?: string;
iconImage?: number;
iconColor?: number;
enabled?: "0" | "1";
visible?: "0" | "1";
minRank?: number;
orderNum?: number;
}): Promise<ActionResult<{ id: number }>> {
export async function createCatalogPage(input: { caption: string; parentId: number; pageLayout?: string; iconImage?: number; iconColor?: number; enabled?: "0" | "1"; visible?: "0" | "1"; minRank?: number; orderNum?: number }): Promise<ActionResult<{ id: number }>> {
const staff = await requireStaff();
const created = await prisma.catalogPages.create({
data: {
@@ -105,7 +83,11 @@ export async function createCatalogPage(input: {
return { ok: true as const, data: { id: created.id } };
}
export async function reorderTreePage(input: { pageId: number; newParentId?: number; newOrderNum: number }) {
export async function reorderTreePage(input: {
pageId: number;
newParentId?: number;
newOrderNum: number;
}) {
await requireStaff();
await prisma.catalogPages.update({
where: { id: input.pageId },
+17 -17
View File
@@ -1,39 +1,39 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { rcon } from "@/lib/services/rcon";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import { rcon } from '@/lib/services/rcon'
const saveEmulatorSettingsSchema = z.object({
settings: z.record(z.string(), z.string()),
});
})
export const saveEmulatorSettings = adminAction(
{ permission: PERMS.SETTINGS_EDIT, schema: saveEmulatorSettingsSchema },
async (ctx) => {
const entries = Object.entries(ctx.data.settings);
const entries = Object.entries(ctx.data.settings)
for (const [key, value] of entries) {
await prisma.emulatorSettings.upsert({
where: { key },
update: { value: String(value) },
create: { key, value: String(value) },
});
})
}
await rcon.updateConfig();
await rcon.updateConfig()
logAudit({
userId: ctx.session.user.id,
action: "emulator_settings_update",
target: "EmulatorSettings",
action: 'emulator_settings_update',
target: 'EmulatorSettings',
after: ctx.data.settings,
});
})
return actionOk();
return actionOk()
},
);
)
+71 -71
View File
@@ -1,18 +1,18 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import {
createEventSchema,
eventPrizeSchema,
eventTypeSchema,
eventWinnerSchema,
updateEventSchema,
} from "@/lib/validators/event";
} from '@/lib/validators/event'
// ── Event Types ─────────────────────────────────────────────────────
@@ -21,63 +21,63 @@ export const createEventType = adminAction(
async (ctx) => {
const eventType = await prisma.websiteEventType.create({
data: ctx.data,
});
})
logAudit({
userId: ctx.session.user.id,
action: "event_type_create",
target: "WebsiteEventType",
action: 'event_type_create',
target: 'WebsiteEventType',
targetId: eventType.id,
after: { name: eventType.name },
});
return actionOk({ id: eventType.id });
})
return actionOk({ id: eventType.id })
},
);
)
const updateEventTypeInput = eventTypeSchema.partial().extend({
id: z.coerce.number().int().positive(),
});
})
export const updateEventType = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: updateEventTypeInput },
async (ctx) => {
const { id, ...data } = ctx.data;
const existing = await prisma.websiteEventType.findUnique({ where: { id } });
if (!existing) throw new ActionError("Event type not found");
const { id, ...data } = ctx.data
const existing = await prisma.websiteEventType.findUnique({ where: { id } })
if (!existing) throw new ActionError('Event type not found')
await prisma.websiteEventType.update({ where: { id }, data });
await prisma.websiteEventType.update({ where: { id }, data })
logAudit({
userId: ctx.session.user.id,
action: "event_type_update",
target: "WebsiteEventType",
action: 'event_type_update',
target: 'WebsiteEventType',
targetId: id,
before: { name: existing.name },
after: data,
});
return actionOk({ id });
})
return actionOk({ id })
},
);
)
const deleteEventTypeInput = z.object({
id: z.coerce.number().int().positive(),
});
})
export const deleteEventType = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: deleteEventTypeInput },
async (ctx) => {
const existing = await prisma.websiteEventType.findUnique({ where: { id: ctx.data.id } });
if (!existing) throw new ActionError("Event type not found");
const existing = await prisma.websiteEventType.findUnique({ where: { id: ctx.data.id } })
if (!existing) throw new ActionError('Event type not found')
await prisma.websiteEventType.delete({ where: { id: ctx.data.id } });
await prisma.websiteEventType.delete({ where: { id: ctx.data.id } })
logAudit({
userId: ctx.session.user.id,
action: "event_type_delete",
target: "WebsiteEventType",
action: 'event_type_delete',
target: 'WebsiteEventType',
targetId: ctx.data.id,
before: { name: existing.name },
});
return actionOk();
})
return actionOk()
},
);
)
// ── Events ──────────────────────────────────────────────────────────
@@ -89,97 +89,97 @@ export const createEvent = adminAction(
...ctx.data,
hostUserId: Number(ctx.session.user.id),
},
});
})
logAudit({
userId: ctx.session.user.id,
action: "event_create",
target: "WebsiteEvent",
action: 'event_create',
target: 'WebsiteEvent',
targetId: event.id,
after: { title: event.title },
});
return actionOk({ id: event.id });
})
return actionOk({ id: event.id })
},
);
)
const updateEventInput = updateEventSchema.extend({
id: z.coerce.number().int().positive(),
});
})
export const updateEvent = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: updateEventInput },
async (ctx) => {
const { id, ...data } = ctx.data;
const existing = await prisma.websiteEvent.findUnique({ where: { id } });
if (!existing) throw new ActionError("Event not found");
const { id, ...data } = ctx.data
const existing = await prisma.websiteEvent.findUnique({ where: { id } })
if (!existing) throw new ActionError('Event not found')
await prisma.websiteEvent.update({ where: { id }, data });
await prisma.websiteEvent.update({ where: { id }, data })
logAudit({
userId: ctx.session.user.id,
action: "event_update",
target: "WebsiteEvent",
action: 'event_update',
target: 'WebsiteEvent',
targetId: id,
before: { title: existing.title, status: existing.status },
after: data,
});
return actionOk({ id });
})
return actionOk({ id })
},
);
)
const deleteEventInput = z.object({
id: z.coerce.number().int().positive(),
});
})
export const deleteEvent = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: deleteEventInput },
async (ctx) => {
const existing = await prisma.websiteEvent.findUnique({ where: { id: ctx.data.id } });
if (!existing) throw new ActionError("Event not found");
const existing = await prisma.websiteEvent.findUnique({ where: { id: ctx.data.id } })
if (!existing) throw new ActionError('Event not found')
await prisma.websiteEvent.delete({ where: { id: ctx.data.id } });
await prisma.websiteEvent.delete({ where: { id: ctx.data.id } })
logAudit({
userId: ctx.session.user.id,
action: "event_delete",
target: "WebsiteEvent",
action: 'event_delete',
target: 'WebsiteEvent',
targetId: ctx.data.id,
before: { title: existing.title },
});
return actionOk();
})
return actionOk()
},
);
)
// ── Prizes ──────────────────────────────────────────────────────────
export const addEventPrize = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: eventPrizeSchema },
async (ctx) => {
const prize = await prisma.websiteEventPrize.create({ data: ctx.data });
return actionOk({ id: prize.id });
const prize = await prisma.websiteEventPrize.create({ data: ctx.data })
return actionOk({ id: prize.id })
},
);
)
const deletePrizeInput = z.object({ id: z.coerce.number().int().positive() });
const deletePrizeInput = z.object({ id: z.coerce.number().int().positive() })
export const deleteEventPrize = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: deletePrizeInput },
async (ctx) => {
await prisma.websiteEventPrize.delete({ where: { id: ctx.data.id } });
return actionOk();
await prisma.websiteEventPrize.delete({ where: { id: ctx.data.id } })
return actionOk()
},
);
)
// ── Winners ─────────────────────────────────────────────────────────
export const addEventWinner = adminAction(
{ permission: PERMS.EVENTS_EDIT, schema: eventWinnerSchema },
async (ctx) => {
const winner = await prisma.websiteEventWinner.create({ data: ctx.data });
const winner = await prisma.websiteEventWinner.create({ data: ctx.data })
logAudit({
userId: ctx.session.user.id,
action: "event_winner_add",
target: "WebsiteEventWinner",
action: 'event_winner_add',
target: 'WebsiteEventWinner',
targetId: winner.id,
after: { eventId: ctx.data.eventId, userId: ctx.data.userId, position: ctx.data.position },
});
return actionOk({ id: winner.id });
})
return actionOk({ id: winner.id })
},
);
)
+2 -16
View File
@@ -3,27 +3,13 @@
import { requireStaff } from "@/lib/admin/guard";
import { prisma } from "@/lib/prisma";
export async function importBadgeFromRemote({
code,
name,
description,
}: {
code: string;
name: string;
description: string;
}) {
export async function importBadgeFromRemote({ code, name, description }: { code: string; name: string; description: string }) {
await requireStaff();
try {
await prisma.websiteBadges.upsert({
where: { badgeKey: code },
update: { badgeName: name, badgeDescription: description, updatedAt: new Date() },
create: {
badgeKey: code,
badgeName: name,
badgeDescription: description,
createdAt: new Date(),
updatedAt: new Date(),
},
create: { badgeKey: code, badgeName: name, badgeDescription: description, createdAt: new Date(), updatedAt: new Date() },
});
return { ok: true as const };
} catch {
+12 -13
View File
@@ -1,20 +1,19 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { adminAction } from "@/lib/safe-action";
import { actionOk } from "@/lib/safe-action-shared";
import { cleanupConvertedSwfs, deleteImportedItem } from "@/lib/services/furni-import";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { adminAction } from '@/lib/safe-action'
import { actionOk } from '@/lib/safe-action-shared'
import { cleanupConvertedSwfs, deleteImportedItem } from '@/lib/services/furni-import'
export const cleanSwfFiles = adminAction({ permission: PERMS.ASSETS_IMPORT }, async () => {
const result = await cleanupConvertedSwfs();
return actionOk(result as unknown as Record<string, unknown>);
});
const result = await cleanupConvertedSwfs()
return actionOk(result as unknown as Record<string, unknown>)
})
const deleteSchema = z.object({ classname: z.string().trim().min(1) });
const deleteSchema = z.object({ classname: z.string().trim().min(1) })
export const deleteImportedFurni = adminAction(
{ permission: PERMS.ASSETS_IMPORT, schema: deleteSchema },
async (ctx) =>
actionOk((await deleteImportedItem(ctx.data.classname)) as unknown as Record<string, unknown>),
);
async (ctx) => actionOk((await deleteImportedItem(ctx.data.classname)) as unknown as Record<string, unknown>),
)
+75 -75
View File
@@ -1,67 +1,67 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { rcon } from "@/lib/services/rcon";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import { rcon } from '@/lib/services/rcon'
// ── CFH Ticket Actions ──────────────────────────────────────────────
const cfhIdSchema = z.object({ ticketId: z.coerce.number().int().positive() });
const cfhIdSchema = z.object({ ticketId: z.coerce.number().int().positive() })
export const assignCfhTicket = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhIdSchema },
async (ctx) => {
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } });
if (!ticket) throw new ActionError("Ticket not found");
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } })
if (!ticket) throw new ActionError('Ticket not found')
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
data: { modId: ctx.session.user.id, state: 1 },
});
})
logAudit({
userId: ctx.session.user.id,
action: "cfh_assign",
target: "support_tickets",
action: 'cfh_assign',
target: 'support_tickets',
targetId: ctx.data.ticketId,
});
})
return actionOk();
return actionOk()
},
);
)
const cfhStateSchema = z.object({
ticketId: z.coerce.number().int().positive(),
state: z.coerce.number().int().min(0).max(3),
});
})
export const updateCfhState = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhStateSchema },
async (ctx) => {
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } });
if (!ticket) throw new ActionError("Ticket not found");
const ticket = await prisma.supportTickets.findUnique({ where: { id: ctx.data.ticketId } })
if (!ticket) throw new ActionError('Ticket not found')
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
data: { state: ctx.data.state, modId: ctx.session.user.id },
});
})
logAudit({
userId: ctx.session.user.id,
action: "cfh_state_change",
target: "support_tickets",
action: 'cfh_state_change',
target: 'support_tickets',
targetId: ctx.data.ticketId,
before: { state: ticket.state },
after: { state: ctx.data.state },
});
})
return actionOk();
return actionOk()
},
);
)
export const closeCfhTicket = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: cfhIdSchema },
@@ -69,138 +69,138 @@ export const closeCfhTicket = adminAction(
await prisma.supportTickets.update({
where: { id: ctx.data.ticketId },
data: { state: 2, modId: ctx.session.user.id },
});
})
logAudit({
userId: ctx.session.user.id,
action: "cfh_close",
target: "support_tickets",
action: 'cfh_close',
target: 'support_tickets',
targetId: ctx.data.ticketId,
});
})
return actionOk();
return actionOk()
},
);
)
// ── Quick Mod Actions ────────────────────────────────────────────────
const userIdSchema = z.object({ userId: z.coerce.number().int().positive() });
const userIdSchema = z.object({ userId: z.coerce.number().int().positive() })
export const quickKick = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: userIdSchema },
async (ctx) => {
await rcon.disconnectUser(ctx.data.userId);
await rcon.disconnectUser(ctx.data.userId)
logAudit({
userId: ctx.session.user.id,
action: "mod_kick",
target: "User",
action: 'mod_kick',
target: 'User',
targetId: ctx.data.userId,
});
})
return actionOk();
return actionOk()
},
);
)
const muteSchema = z.object({
userId: z.coerce.number().int().positive(),
duration: z.coerce.number().int().min(0).max(525600), // max 1 year in minutes
});
})
export const quickMute = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: muteSchema },
async (ctx) => {
await rcon.muteUser(ctx.data.userId, ctx.data.duration);
await rcon.muteUser(ctx.data.userId, ctx.data.duration)
logAudit({
userId: ctx.session.user.id,
action: "mod_mute",
target: "User",
action: 'mod_mute',
target: 'User',
targetId: ctx.data.userId,
after: { duration: ctx.data.duration },
});
})
return actionOk();
return actionOk()
},
);
)
export const quickUnmute = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: userIdSchema },
async (ctx) => {
await rcon.unmuteUser(ctx.data.userId);
await rcon.unmuteUser(ctx.data.userId)
logAudit({
userId: ctx.session.user.id,
action: "mod_unmute",
target: "User",
action: 'mod_unmute',
target: 'User',
targetId: ctx.data.userId,
});
})
return actionOk();
return actionOk()
},
);
)
const alertSchema = z.object({
userId: z.coerce.number().int().positive(),
message: z.string().min(1).max(500),
});
})
export const quickAlert = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: alertSchema },
async (ctx) => {
await rcon.alertUser(ctx.data.userId, ctx.data.message);
await rcon.alertUser(ctx.data.userId, ctx.data.message)
logAudit({
userId: ctx.session.user.id,
action: "mod_alert",
target: "User",
action: 'mod_alert',
target: 'User',
targetId: ctx.data.userId,
after: { message: ctx.data.message },
});
})
return actionOk();
return actionOk()
},
);
)
const roomIdSchema = z.object({ roomId: z.coerce.number().int().positive() });
const roomIdSchema = z.object({ roomId: z.coerce.number().int().positive() })
export const quickRoomKick = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: roomIdSchema },
async (ctx) => {
await rcon.kickAll(ctx.data.roomId);
await rcon.kickAll(ctx.data.roomId)
logAudit({
userId: ctx.session.user.id,
action: "mod_room_kick",
target: "Room",
action: 'mod_room_kick',
target: 'Room',
targetId: ctx.data.roomId,
});
})
return actionOk();
return actionOk()
},
);
)
const broadcastSchema = z.object({
message: z.string().min(1).max(500),
type: z.enum(["hotel", "staff"]),
});
type: z.enum(['hotel', 'staff']),
})
export const broadcastAlert = adminAction(
{ permission: PERMS.MODERATION_EDIT, schema: broadcastSchema },
async (ctx) => {
if (ctx.data.type === "hotel") {
await rcon.hotelAlert(ctx.data.message);
if (ctx.data.type === 'hotel') {
await rcon.hotelAlert(ctx.data.message)
} else {
await rcon.staffAlert(ctx.data.message);
await rcon.staffAlert(ctx.data.message)
}
logAudit({
userId: ctx.session.user.id,
action: `mod_broadcast_${ctx.data.type}`,
target: "broadcast",
target: 'broadcast',
after: { message: ctx.data.message },
});
})
return actionOk();
return actionOk()
},
);
)
+52 -90
View File
@@ -1,140 +1,102 @@
"use server";
'use server'
import { revalidateTag } from "next/cache";
import { z } from "zod";
import { PERMS } from "@/lib/permission-slugs";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { createEmulatorRank, deleteEmulatorRank, updateEmulatorRank } from "@/lib/services/permission-ranks";
import { rcon } from "@/lib/services/rcon";
import { logStaffActivity } from "@/lib/services/staff-activity";
import { revalidateTag } from 'next/cache'
import { z } from 'zod'
import { PERMS } from '@/lib/permission-slugs'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { createEmulatorRank, deleteEmulatorRank, updateEmulatorRank } from '@/lib/services/permission-ranks'
import { rcon } from '@/lib/services/rcon'
import { logStaffActivity } from '@/lib/services/staff-activity'
const createRankSchema = z.object({
rank_name: z.string().trim().min(1).max(25),
level: z.coerce.number().int().min(1),
});
})
export const createRank = adminAction(
{ schema: createRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const id = await createEmulatorRank(prisma, ctx.data);
const id = await createEmulatorRank(prisma, ctx.data)
await prisma.aclRole.upsert({
where: { slug: `rank_${id}` },
create: {
slug: `rank_${id}`,
title: ctx.data.rank_name,
description: "CMS role synchronized from permission_ranks",
},
create: { slug: `rank_${id}`, title: ctx.data.rank_name, description: 'CMS role synchronized from permission_ranks' },
update: { title: ctx.data.rank_name },
});
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_create",
description: `Created rank #${id}`,
targetType: "rank",
targetId: id,
});
await rcon.send("updatepermissions");
revalidateTag("permissions", { expire: 0 });
return actionOk({ id });
})
await logStaffActivity({ staffId: ctx.session.user.id, action: 'rank_create', description: `Created rank #${id}`, targetType: 'rank', targetId: id })
await rcon.send('updatepermissions')
revalidateTag('permissions', { expire: 0 })
return actionOk({ id })
},
);
)
const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() });
const deleteRankSchema = z.object({ id: z.coerce.number().int().positive() })
export const deleteRank = adminAction(
{ schema: deleteRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const users = await prisma.user.count({ where: { rank: ctx.data.id } });
if (users > 0) throw new ActionError(`Cannot delete: ${users} users have this rank`);
const role = await prisma.aclRole.findFirst({ where: { slug: `rank_${ctx.data.id}` } });
await deleteEmulatorRank(prisma, ctx.data.id);
const users = await prisma.user.count({ where: { rank: ctx.data.id } })
if (users > 0) throw new ActionError(`Cannot delete: ${users} users have this rank`)
const role = await prisma.aclRole.findFirst({ where: { slug: `rank_${ctx.data.id}` } })
await deleteEmulatorRank(prisma, ctx.data.id)
if (role) {
await prisma.$transaction([
prisma.aclModelPermission.deleteMany({ where: { modelId: role.id, modelType: "Role" } }),
prisma.aclModelPermission.deleteMany({ where: { modelId: role.id, modelType: 'Role' } }),
prisma.aclModelRole.deleteMany({ where: { roleId: role.id } }),
prisma.aclRole.delete({ where: { id: role.id } }),
]);
])
}
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_delete",
description: `Deleted rank #${ctx.data.id}`,
targetType: "rank",
targetId: ctx.data.id,
});
await rcon.send("updatepermissions");
revalidateTag("permissions", { expire: 0 });
return actionOk();
await logStaffActivity({ staffId: ctx.session.user.id, action: 'rank_delete', description: `Deleted rank #${ctx.data.id}`, targetType: 'rank', targetId: ctx.data.id })
await rcon.send('updatepermissions')
revalidateTag('permissions', { expire: 0 })
return actionOk()
},
);
)
const saveRankSchema = z.object({
id: z.coerce.number().int().positive(),
fields: z.record(z.string(), z.union([z.string(), z.number()])),
});
})
export const saveRank = adminAction(
{ schema: saveRankSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
await updateEmulatorRank(prisma, ctx.data.id, ctx.data.fields);
if (typeof ctx.data.fields.rank_name === "string") {
await prisma.aclRole.updateMany({
where: { slug: `rank_${ctx.data.id}` },
data: { title: ctx.data.fields.rank_name },
});
await updateEmulatorRank(prisma, ctx.data.id, ctx.data.fields)
if (typeof ctx.data.fields.rank_name === 'string') {
await prisma.aclRole.updateMany({ where: { slug: `rank_${ctx.data.id}` }, data: { title: ctx.data.fields.rank_name } })
}
await logStaffActivity({
staffId: ctx.session.user.id,
action: "rank_update",
description: `Updated rank #${ctx.data.id}`,
targetType: "rank",
targetId: ctx.data.id,
});
await rcon.send("updatepermissions");
revalidateTag("permissions", { expire: 0 });
return actionOk();
await logStaffActivity({ staffId: ctx.session.user.id, action: 'rank_update', description: `Updated rank #${ctx.data.id}`, targetType: 'rank', targetId: ctx.data.id })
await rcon.send('updatepermissions')
revalidateTag('permissions', { expire: 0 })
return actionOk()
},
);
)
const setCmsPermsSchema = z.object({
roleId: z.coerce.number().int().positive(),
permissionSlugs: z.array(z.string().trim().min(1)).max(500),
});
})
export const setCmsPermissions = adminAction(
{ schema: setCmsPermsSchema, permission: PERMS.PERMISSIONS_MANAGE },
async (ctx) => {
const role = await prisma.aclRole.findUnique({
where: { id: ctx.data.roleId },
select: { id: true, slug: true },
});
if (!role) throw new ActionError("Role not found");
const role = await prisma.aclRole.findUnique({ where: { id: ctx.data.roleId }, select: { id: true, slug: true } })
if (!role) throw new ActionError('Role not found')
const permissions = await prisma.aclPermission.findMany({
where: { slug: { in: ctx.data.permissionSlugs } },
select: { id: true },
});
})
await prisma.$transaction(async (tx) => {
await tx.aclModelPermission.deleteMany({ where: { modelId: role.id, modelType: "Role" } });
await tx.aclModelPermission.deleteMany({ where: { modelId: role.id, modelType: 'Role' } })
if (permissions.length) {
await tx.aclModelPermission.createMany({
data: permissions.map((permission) => ({
modelId: role.id,
modelType: "Role",
permissionId: permission.id,
})),
});
data: permissions.map((permission) => ({ modelId: role.id, modelType: 'Role', permissionId: permission.id })),
})
}
});
await logStaffActivity({
staffId: ctx.session.user.id,
action: "acl_role_permissions_update",
description: `Updated ${permissions.length} permissions for ${role.slug}`,
targetType: "acl_role",
targetId: role.id,
});
revalidateTag("permissions", { expire: 0 });
return actionOk();
})
await logStaffActivity({ staffId: ctx.session.user.id, action: 'acl_role_permissions_update', description: `Updated ${permissions.length} permissions for ${role.slug}`, targetType: 'acl_role', targetId: role.id })
revalidateTag('permissions', { expire: 0 })
return actionOk()
},
);
)
+45 -45
View File
@@ -1,107 +1,107 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { createPollSchema, pollQuestionSchema, updatePollSchema } from "@/lib/validators/poll";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import { createPollSchema, pollQuestionSchema, updatePollSchema } from '@/lib/validators/poll'
// ── Polls ───────────────────────────────────────────────────────────
export const createPoll = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: createPollSchema },
async (ctx) => {
const poll = await prisma.websitePoll.create({ data: ctx.data });
const poll = await prisma.websitePoll.create({ data: ctx.data })
logAudit({
userId: ctx.session.user.id,
action: "poll_create",
target: "WebsitePoll",
action: 'poll_create',
target: 'WebsitePoll',
targetId: poll.id,
after: { title: poll.title },
});
return actionOk({ id: poll.id });
})
return actionOk({ id: poll.id })
},
);
)
const updatePollInput = updatePollSchema.extend({
id: z.coerce.number().int().positive(),
});
})
export const updatePoll = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: updatePollInput },
async (ctx) => {
const { id, ...data } = ctx.data;
const existing = await prisma.websitePoll.findUnique({ where: { id } });
if (!existing) throw new ActionError("Poll not found");
const { id, ...data } = ctx.data
const existing = await prisma.websitePoll.findUnique({ where: { id } })
if (!existing) throw new ActionError('Poll not found')
await prisma.websitePoll.update({ where: { id }, data });
await prisma.websitePoll.update({ where: { id }, data })
logAudit({
userId: ctx.session.user.id,
action: "poll_update",
target: "WebsitePoll",
action: 'poll_update',
target: 'WebsitePoll',
targetId: id,
before: { title: existing.title, status: existing.status },
after: data,
});
return actionOk({ id });
})
return actionOk({ id })
},
);
)
const deletePollInput = z.object({
id: z.coerce.number().int().positive(),
});
})
export const deletePoll = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: deletePollInput },
async (ctx) => {
const existing = await prisma.websitePoll.findUnique({ where: { id: ctx.data.id } });
if (!existing) throw new ActionError("Poll not found");
const existing = await prisma.websitePoll.findUnique({ where: { id: ctx.data.id } })
if (!existing) throw new ActionError('Poll not found')
await prisma.websitePoll.delete({ where: { id: ctx.data.id } });
await prisma.websitePoll.delete({ where: { id: ctx.data.id } })
logAudit({
userId: ctx.session.user.id,
action: "poll_delete",
target: "WebsitePoll",
action: 'poll_delete',
target: 'WebsitePoll',
targetId: ctx.data.id,
before: { title: existing.title },
});
return actionOk();
})
return actionOk()
},
);
)
// ── Questions ───────────────────────────────────────────────────────
export const addPollQuestion = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: pollQuestionSchema },
async (ctx) => {
const question = await prisma.websitePollQuestion.create({ data: ctx.data });
return actionOk({ id: question.id });
const question = await prisma.websitePollQuestion.create({ data: ctx.data })
return actionOk({ id: question.id })
},
);
)
const updateQuestionInput = pollQuestionSchema.partial().extend({
id: z.coerce.number().int().positive(),
});
})
export const updatePollQuestion = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: updateQuestionInput },
async (ctx) => {
const { id, ...data } = ctx.data;
await prisma.websitePollQuestion.update({ where: { id }, data });
return actionOk({ id });
const { id, ...data } = ctx.data
await prisma.websitePollQuestion.update({ where: { id }, data })
return actionOk({ id })
},
);
)
const deleteQuestionInput = z.object({
id: z.coerce.number().int().positive(),
});
})
export const deletePollQuestion = adminAction(
{ permission: PERMS.POLLS_EDIT, schema: deleteQuestionInput },
async (ctx) => {
await prisma.websitePollQuestion.delete({ where: { id: ctx.data.id } });
return actionOk();
await prisma.websitePollQuestion.delete({ where: { id: ctx.data.id } })
return actionOk()
},
);
)
+45 -45
View File
@@ -1,10 +1,10 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
// Models custom_prefixes / custom_prefix_blacklist / custom_prefix_settings
// are not represented in prisma/schema.prisma yet — we use raw queries with
@@ -19,26 +19,26 @@ const createPrefixSchema = z.object({
icon: z.string().optional(),
effect: z.string().optional(),
active: z.coerce.number().int().min(0).max(1).default(1),
});
})
export const createPrefix = adminAction(
{ permission: PERMS.PREFIXES_EDIT, schema: createPrefixSchema },
async (ctx) => {
const { username, text, color, icon, effect, active } = ctx.data;
const { username, text, color, icon, effect, active } = ctx.data
const users = await prisma.$queryRaw<{ id: number }[]>`
SELECT id FROM users WHERE username = ${username} LIMIT 1
`;
if (users.length === 0) throw new ActionError("User not found");
`
if (users.length === 0) throw new ActionError('User not found')
await prisma.$executeRaw`
INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
VALUES (${users[0].id}, ${text}, ${color}, ${icon || ""}, ${effect || ""}, ${active})
`;
VALUES (${users[0].id}, ${text}, ${color}, ${icon || ''}, ${effect || ''}, ${active})
`
return actionOk();
return actionOk()
},
);
)
// ── Update prefix ───────────────────────────────────────────────────
@@ -49,94 +49,94 @@ const updatePrefixSchema = z.object({
icon: z.string().optional(),
effect: z.string().optional(),
active: z.coerce.number().int().min(0).max(1).optional(),
});
})
export const updatePrefix = adminAction(
{ permission: PERMS.PREFIXES_EDIT, schema: updatePrefixSchema },
async (ctx) => {
const { id, text, color, icon, effect, active } = ctx.data;
const { id, text, color, icon, effect, active } = ctx.data
await prisma.$executeRaw`
UPDATE custom_prefixes
SET text = ${text}, color = ${color}, icon = ${icon || ""}, effect = ${effect || ""}, active = ${active ?? 1}
SET text = ${text}, color = ${color}, icon = ${icon || ''}, effect = ${effect || ''}, active = ${active ?? 1}
WHERE id = ${id}
`;
`
return actionOk();
return actionOk()
},
);
)
// ── Delete prefix ───────────────────────────────────────────────────
const deletePrefixSchema = z.object({
id: z.coerce.number().int().positive(),
});
})
export const deletePrefix = adminAction(
{ permission: PERMS.PREFIXES_EDIT, schema: deletePrefixSchema },
async (ctx) => {
await prisma.$executeRaw`DELETE FROM custom_prefixes WHERE id = ${ctx.data.id}`;
return actionOk();
await prisma.$executeRaw`DELETE FROM custom_prefixes WHERE id = ${ctx.data.id}`
return actionOk()
},
);
)
// ── Add blacklist word ──────────────────────────────────────────────
const addBlacklistWordSchema = z.object({
word: z.string().min(1).max(100),
});
})
export const addBlacklistWord = adminAction(
{ permission: PERMS.PREFIXES_EDIT, schema: addBlacklistWordSchema },
async (ctx) => {
await prisma.$executeRaw`
INSERT INTO custom_prefix_blacklist (word) VALUES (${ctx.data.word.trim()})
`;
return actionOk();
`
return actionOk()
},
);
)
// ── Remove blacklist word ───────────────────────────────────────────
const removeBlacklistWordSchema = z.object({
id: z.coerce.number().int().positive(),
});
})
export const removeBlacklistWord = adminAction(
{ permission: PERMS.PREFIXES_EDIT, schema: removeBlacklistWordSchema },
async (ctx) => {
await prisma.$executeRaw`DELETE FROM custom_prefix_blacklist WHERE id = ${ctx.data.id}`;
return actionOk();
await prisma.$executeRaw`DELETE FROM custom_prefix_blacklist WHERE id = ${ctx.data.id}`
return actionOk()
},
);
)
// ── Update prefix settings ──────────────────────────────────────────
const SETTINGS_WHITELIST = new Set([
"enabled",
"max_length",
"min_rank",
"allow_colors",
"allow_bold",
"allow_italic",
"default_color",
]);
'enabled',
'max_length',
'min_rank',
'allow_colors',
'allow_bold',
'allow_italic',
'default_color',
])
const updatePrefixSettingsSchema = z.object({
settings: z.record(z.string(), z.string()),
});
})
export const updatePrefixSettings = adminAction(
{ permission: PERMS.PREFIXES_EDIT, schema: updatePrefixSettingsSchema },
async (ctx) => {
for (const [key, value] of Object.entries(ctx.data.settings)) {
if (!SETTINGS_WHITELIST.has(key)) continue;
if (!SETTINGS_WHITELIST.has(key)) continue
await prisma.$executeRaw`
INSERT INTO custom_prefix_settings (\`key\`, \`value\`)
VALUES (${key}, ${value})
ON DUPLICATE KEY UPDATE \`value\` = ${value}
`;
`
}
return actionOk();
return actionOk()
},
);
)
+1 -10
View File
@@ -71,16 +71,7 @@ export async function deleteRoom({ id }: { id: number }) {
revalidatePath("/admin/rooms");
}
export async function updateRoom({
id,
...data
}: {
id: number;
name?: string;
description?: string;
state?: string;
usersMax?: number;
}) {
export async function updateRoom({ id, ...data }: { id: number; name?: string; description?: string; state?: string; usersMax?: number }) {
const staff = await requireStaff();
await prisma.rooms.update({ where: { id }, data: data as any });
await logStaffActivity({
+1 -13
View File
@@ -10,19 +10,7 @@ export async function deleteSoundtrack({ id }: { id: number }) {
revalidatePath("/admin/sounds");
}
export async function updateSoundtrack({
id,
name,
author,
track,
length,
}: {
id: number;
name: string;
author: string;
track: string;
length: number;
}) {
export async function updateSoundtrack({ id, name, author, track, length }: { id: number; name: string; author: string; track: string; length: number }) {
await requireStaff();
await prisma.soundtracks.update({
where: { id },
+24 -22
View File
@@ -1,45 +1,47 @@
"use server";
'use server'
import { z } from "zod";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { z } from 'zod'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
const templateSchema = z.object({
title: z.string().min(1).max(255),
content: z.string().min(1),
category: z.string().max(50).optional().default("general"),
category: z.string().max(50).optional().default('general'),
sortOrder: z.coerce.number().int().min(0).default(0),
});
})
export const createTemplate = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: templateSchema },
async (ctx) => {
const tpl = await prisma.websiteTicketTemplate.create({ data: ctx.data });
return actionOk({ id: tpl.id });
const tpl = await prisma.websiteTicketTemplate.create({ data: ctx.data })
return actionOk({ id: tpl.id })
},
);
)
const updateTemplateInput = templateSchema.partial().extend({ id: z.coerce.number().int().positive() });
const updateTemplateInput = templateSchema
.partial()
.extend({ id: z.coerce.number().int().positive() })
export const updateTemplate = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: updateTemplateInput },
async (ctx) => {
const { id, ...data } = ctx.data;
const existing = await prisma.websiteTicketTemplate.findUnique({ where: { id } });
if (!existing) throw new ActionError("Template not found");
await prisma.websiteTicketTemplate.update({ where: { id }, data });
return actionOk({ id });
const { id, ...data } = ctx.data
const existing = await prisma.websiteTicketTemplate.findUnique({ where: { id } })
if (!existing) throw new ActionError('Template not found')
await prisma.websiteTicketTemplate.update({ where: { id }, data })
return actionOk({ id })
},
);
)
const deleteTemplateInput = z.object({ id: z.coerce.number().int().positive() });
const deleteTemplateInput = z.object({ id: z.coerce.number().int().positive() })
export const deleteTemplate = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: deleteTemplateInput },
async (ctx) => {
await prisma.websiteTicketTemplate.delete({ where: { id: ctx.data.id } });
return actionOk();
await prisma.websiteTicketTemplate.delete({ where: { id: ctx.data.id } })
return actionOk()
},
);
)
+73 -73
View File
@@ -1,18 +1,18 @@
"use server";
'use server'
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction, authAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { notify } from "@/lib/services/webhook";
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction, authAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import { notify } from '@/lib/services/webhook'
import {
assignTicketSchema,
createTicketSchema,
replyTicketSchema,
updateTicketPrioritySchema,
updateTicketStatusSchema,
} from "@/lib/validators/ticket";
} from '@/lib/validators/ticket'
// ── User actions (authenticated, no admin perms needed) ──────────────
@@ -23,7 +23,7 @@ export const createTicket = authAction({ schema: createTicketSchema }, async (ct
category: ctx.data.category,
creatorId: ctx.session.user.id,
},
});
})
// Create the first message
await prisma.websiteTicketMessage.create({
@@ -33,26 +33,26 @@ export const createTicket = authAction({ schema: createTicketSchema }, async (ct
message: ctx.data.message,
isStaff: 0,
},
});
})
notify({
action: "ticket_create",
action: 'ticket_create',
actor: ctx.session.user.username,
target: `#${ticket.id} - ${ticket.subject}`,
details: `Category: ${ticket.category}`,
});
})
return actionOk({ id: ticket.id });
});
return actionOk({ id: ticket.id })
})
export const userReplyTicket = authAction({ schema: replyTicketSchema }, async (ctx) => {
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ctx.data.ticketId },
});
})
if (!ticket) throw new ActionError("Ticket not found");
if (ticket.creatorId !== ctx.session.user.id) throw new ActionError("Unauthorized");
if (ticket.status === "closed") throw new ActionError("Ticket is closed");
if (!ticket) throw new ActionError('Ticket not found')
if (ticket.creatorId !== ctx.session.user.id) throw new ActionError('Unauthorized')
if (ticket.status === 'closed') throw new ActionError('Ticket is closed')
await prisma.websiteTicketMessage.create({
data: {
@@ -61,38 +61,38 @@ export const userReplyTicket = authAction({ schema: replyTicketSchema }, async (
message: ctx.data.message,
isStaff: 0,
},
});
})
// If ticket was in "waiting" (waiting for user), move back to open
if (ticket.status === "waiting") {
if (ticket.status === 'waiting') {
await prisma.websiteTicket.update({
where: { id: ctx.data.ticketId },
data: { status: "open" },
});
data: { status: 'open' },
})
}
return actionOk();
});
return actionOk()
})
export const closeTicketByUser = authAction(
{ schema: replyTicketSchema.pick({ ticketId: true }) },
async (ctx) => {
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ctx.data.ticketId },
});
})
if (!ticket) throw new ActionError("Ticket not found");
if (ticket.creatorId !== ctx.session.user.id) throw new ActionError("Unauthorized");
if (ticket.status === "closed") throw new ActionError("Ticket is already closed");
if (!ticket) throw new ActionError('Ticket not found')
if (ticket.creatorId !== ctx.session.user.id) throw new ActionError('Unauthorized')
if (ticket.status === 'closed') throw new ActionError('Ticket is already closed')
await prisma.websiteTicket.update({
where: { id: ctx.data.ticketId },
data: { status: "closed", closedAt: new Date() },
});
data: { status: 'closed', closedAt: new Date() },
})
return actionOk();
return actionOk()
},
);
)
// ── Admin actions ────────────────────────────────────────────────────
@@ -101,9 +101,9 @@ export const adminReplyTicket = adminAction(
async (ctx) => {
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ctx.data.ticketId },
});
})
if (!ticket) throw new ActionError("Ticket not found");
if (!ticket) throw new ActionError('Ticket not found')
await prisma.websiteTicketMessage.create({
data: {
@@ -112,118 +112,118 @@ export const adminReplyTicket = adminAction(
message: ctx.data.message,
isStaff: 1,
},
});
})
// Auto-assign if not assigned yet
const updates: Record<string, unknown> = { status: "waiting" };
const updates: Record<string, unknown> = { status: 'waiting' }
if (!ticket.assigneeId) {
updates.assigneeId = ctx.session.user.id;
updates.assigneeId = ctx.session.user.id
}
await prisma.websiteTicket.update({
where: { id: ctx.data.ticketId },
data: updates,
});
})
logAudit({
userId: ctx.session.user.id,
action: "ticket_reply",
target: "WebsiteTicket",
action: 'ticket_reply',
target: 'WebsiteTicket',
targetId: ctx.data.ticketId,
});
})
return actionOk();
return actionOk()
},
);
)
export const updateTicketStatus = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: updateTicketStatusSchema },
async (ctx) => {
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ctx.data.ticketId },
});
})
if (!ticket) throw new ActionError("Ticket not found");
if (!ticket) throw new ActionError('Ticket not found')
const data: Record<string, unknown> = { status: ctx.data.status };
if (ctx.data.status === "closed") {
data.closedAt = new Date();
const data: Record<string, unknown> = { status: ctx.data.status }
if (ctx.data.status === 'closed') {
data.closedAt = new Date()
}
if (ctx.data.status === "in_progress" && !ticket.assigneeId) {
data.assigneeId = ctx.session.user.id;
if (ctx.data.status === 'in_progress' && !ticket.assigneeId) {
data.assigneeId = ctx.session.user.id
}
await prisma.websiteTicket.update({
where: { id: ctx.data.ticketId },
data,
});
})
logAudit({
userId: ctx.session.user.id,
action: "ticket_status_change",
target: "WebsiteTicket",
action: 'ticket_status_change',
target: 'WebsiteTicket',
targetId: ctx.data.ticketId,
before: { status: ticket.status },
after: { status: ctx.data.status },
});
})
return actionOk();
return actionOk()
},
);
)
export const assignTicket = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: assignTicketSchema },
async (ctx) => {
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ctx.data.ticketId },
});
})
if (!ticket) throw new ActionError("Ticket not found");
if (!ticket) throw new ActionError('Ticket not found')
await prisma.websiteTicket.update({
where: { id: ctx.data.ticketId },
data: {
assigneeId: ctx.data.assigneeId,
status: ctx.data.assigneeId ? "in_progress" : "open",
status: ctx.data.assigneeId ? 'in_progress' : 'open',
},
});
})
logAudit({
userId: ctx.session.user.id,
action: "ticket_assign",
target: "WebsiteTicket",
action: 'ticket_assign',
target: 'WebsiteTicket',
targetId: ctx.data.ticketId,
before: { assigneeId: ticket.assigneeId },
after: { assigneeId: ctx.data.assigneeId },
});
})
return actionOk();
return actionOk()
},
);
)
export const updateTicketPriority = adminAction(
{ permission: PERMS.TICKETS_EDIT, schema: updateTicketPrioritySchema },
async (ctx) => {
const ticket = await prisma.websiteTicket.findUnique({
where: { id: ctx.data.ticketId },
});
})
if (!ticket) throw new ActionError("Ticket not found");
if (!ticket) throw new ActionError('Ticket not found')
await prisma.websiteTicket.update({
where: { id: ctx.data.ticketId },
data: { priority: ctx.data.priority },
});
})
logAudit({
userId: ctx.session.user.id,
action: "ticket_priority_change",
target: "WebsiteTicket",
action: 'ticket_priority_change',
target: 'WebsiteTicket',
targetId: ctx.data.ticketId,
before: { priority: ticket.priority },
after: { priority: ctx.data.priority },
});
})
return actionOk();
return actionOk()
},
);
)
+56 -53
View File
@@ -1,73 +1,76 @@
"use server";
'use server'
import fs from "node:fs/promises";
import path from "node:path";
import JSON5 from "json5";
import { z } from "zod";
import { CLIENT_TRANSLATION_FILES, getClientTranslationFile } from "@/lib/client-translation-files";
import { patchJson5 } from "@/lib/json5-patch";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import fs from 'node:fs/promises'
import path from 'node:path'
import JSON5 from 'json5'
import { z } from 'zod'
import { CLIENT_TRANSLATION_FILES, getClientTranslationFile } from '@/lib/client-translation-files'
import { patchJson5 } from '@/lib/json5-patch'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
const saveTranslationsSchema = z.object({
locale: z.enum(["en", "it"]),
locale: z.enum(['en', 'it']),
data: z.record(z.string(), z.unknown()),
});
})
export const saveTranslations = adminAction({ schema: saveTranslationsSchema }, async (ctx) => {
if (ctx.session.user.rank < 7) throw new ActionError("Forbidden");
if (ctx.session.user.rank < 7) throw new ActionError('Forbidden')
const filePath = path.join(process.cwd(), "messages", `${ctx.data.locale}.json`);
await fs.writeFile(filePath, JSON.stringify(ctx.data.data, null, 2), "utf-8");
const filePath = path.join(process.cwd(), 'messages', `${ctx.data.locale}.json`)
await fs.writeFile(filePath, JSON.stringify(ctx.data.data, null, 2), 'utf-8')
return actionOk();
});
return actionOk()
})
const saveClientTranslationsSchema = z.object({
fileId: z.enum(CLIENT_TRANSLATION_FILES.map((f) => f.id) as [string, ...string[]]),
data: z.record(z.string(), z.string()),
});
})
export const saveClientTranslations = adminAction({ schema: saveClientTranslationsSchema }, async (ctx) => {
if (ctx.session.user.rank < 7) throw new ActionError("Forbidden");
export const saveClientTranslations = adminAction(
{ schema: saveClientTranslationsSchema },
async (ctx) => {
if (ctx.session.user.rank < 7) throw new ActionError('Forbidden')
const file = getClientTranslationFile(ctx.data.fileId);
if (!file) throw new ActionError("Unknown file");
if (file.readOnly) throw new ActionError("File is read-only");
const file = getClientTranslationFile(ctx.data.fileId)
if (!file) throw new ActionError('Unknown file')
if (file.readOnly) throw new ActionError('File is read-only')
// file.relPath comes from CLIENT_TRANSLATION_FILES (closed enum) but
// Turbopack's static tracer can't prove that — without the hint it
// pulls the entire project into the NFT list.
const absPath = path.join(/*turbopackIgnore: true*/ process.cwd(), file.relPath);
const raw = await fs.readFile(absPath, "utf-8");
// file.relPath comes from CLIENT_TRANSLATION_FILES (closed enum) but
// Turbopack's static tracer can't prove that — without the hint it
// pulls the entire project into the NFT list.
const absPath = path.join(/*turbopackIgnore: true*/ process.cwd(), file.relPath)
const raw = await fs.readFile(absPath, 'utf-8')
if (file.format === "json") {
// Plain JSON — no comments to preserve, just round-trip.
await fs.writeFile(absPath, JSON.stringify(ctx.data.data, null, 4), "utf-8");
return actionOk({ commentsLost: false, unpatchedKeys: [] as string[] });
}
// JSON5: surgical line-level patch keeps headers and section comments
// intact. Falls back to a full re-serialization (which DOES drop comments)
// only when an edited key cannot be located via the patch contract.
const original: Record<string, string> = {};
const parsed = JSON5.parse(raw);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
for (const [k, v] of Object.entries(parsed)) {
original[k] = v == null ? "" : String(v);
if (file.format === 'json') {
// Plain JSON — no comments to preserve, just round-trip.
await fs.writeFile(absPath, JSON.stringify(ctx.data.data, null, 4), 'utf-8')
return actionOk({ commentsLost: false, unpatchedKeys: [] as string[] })
}
}
const { content, unpatchedKeys } = patchJson5(raw, original, ctx.data.data);
// JSON5: surgical line-level patch keeps headers and section comments
// intact. Falls back to a full re-serialization (which DOES drop comments)
// only when an edited key cannot be located via the patch contract.
const original: Record<string, string> = {}
const parsed = JSON5.parse(raw)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
for (const [k, v] of Object.entries(parsed)) {
original[k] = v == null ? '' : String(v)
}
}
if (unpatchedKeys.length === 0) {
await fs.writeFile(absPath, content, "utf-8");
return actionOk({ commentsLost: false, unpatchedKeys });
}
const { content, unpatchedKeys } = patchJson5(raw, original, ctx.data.data)
// At least one key could not be patched surgically (e.g. unusual
// formatting or a brand-new key). Fall back to a full re-serialization
// and warn the caller that comments were lost.
await fs.writeFile(absPath, JSON5.stringify(ctx.data.data, null, 4), "utf-8");
return actionOk({ commentsLost: true, unpatchedKeys });
});
if (unpatchedKeys.length === 0) {
await fs.writeFile(absPath, content, 'utf-8')
return actionOk({ commentsLost: false, unpatchedKeys })
}
// At least one key could not be patched surgically (e.g. unusual
// formatting or a brand-new key). Fall back to a full re-serialization
// and warn the caller that comments were lost.
await fs.writeFile(absPath, JSON5.stringify(ctx.data.data, null, 4), 'utf-8')
return actionOk({ commentsLost: true, unpatchedKeys })
},
)
+213 -192
View File
@@ -1,31 +1,36 @@
"use server";
'use server'
import crypto from "node:crypto";
import { hash } from "bcryptjs";
import { z } from "zod";
import { Prisma } from "@/generated/prisma/client";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { ActionError, actionOk } from "@/lib/safe-action-shared";
import { logAudit } from "@/lib/services/audit";
import { rcon } from "@/lib/services/rcon";
import { notify } from "@/lib/services/webhook";
import { banUserSchema, createUserSchema, giveBadgeSchema, updateUserSchema } from "@/lib/validators/user";
import crypto from 'node:crypto'
import { hash } from 'bcryptjs'
import { z } from 'zod'
import { Prisma } from '@/generated/prisma/client'
import { PERMS } from '@/lib/permissions'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { ActionError, actionOk } from '@/lib/safe-action-shared'
import { logAudit } from '@/lib/services/audit'
import { rcon } from '@/lib/services/rcon'
import { notify } from '@/lib/services/webhook'
import {
banUserSchema,
createUserSchema,
giveBadgeSchema,
updateUserSchema,
} from '@/lib/validators/user'
const DEFAULT_LOOK = "hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64";
const DEFAULT_LOOK = 'hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64'
export const createUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: createUserSchema },
async (ctx) => {
const { username, mail, password, rank, motto } = ctx.data;
const { username, mail, password, rank, motto } = ctx.data
if (rank >= ctx.session.user.rank && ctx.session.user.rank < 7) {
throw new ActionError("Cannot assign rank equal or higher than your own");
throw new ActionError('Cannot assign rank equal or higher than your own')
}
const hashedPassword = await hash(password, 12);
const now = Math.floor(Date.now() / 1000);
const hashedPassword = await hash(password, 12)
const now = Math.floor(Date.now() / 1000)
try {
const user = await prisma.$transaction(async (tx) => {
@@ -40,211 +45,221 @@ export const createUser = adminAction(
credits: 5000,
pixels: 5000,
accountCreated: now,
ipRegister: "0.0.0.0",
ipCurrent: "0.0.0.0",
ipRegister: '0.0.0.0',
ipCurrent: '0.0.0.0',
},
});
})
await tx.usersSettings.create({ data: { userId: created.id } });
await tx.usersSettings.create({ data: { userId: created.id } })
await tx.usersCurrency.createMany({
data: [
{ userId: created.id, type: 0, amount: 5000 },
{ userId: created.id, type: 5, amount: 5000 },
],
});
})
return created;
});
return created
})
logAudit({
userId: ctx.session.user.id,
action: "user_create",
target: "User",
action: 'user_create',
target: 'User',
targetId: user.id,
after: { username, mail, rank },
});
})
notify({
action: "user_edit",
action: 'user_edit',
actor: ctx.session.user.username,
target: username,
targetId: user.id,
details: "Account created by admin",
});
details: 'Account created by admin',
})
return actionOk({ id: user.id, username: user.username });
return actionOk({ id: user.id, username: user.username })
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
const target = (err.meta?.target as string[]) ?? [];
if (target.includes("username")) throw new ActionError("Username already taken");
if (target.includes("mail")) throw new ActionError("Email already registered");
throw new ActionError("Username or email already in use");
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
const target = (err.meta?.target as string[]) ?? []
if (target.includes('username')) throw new ActionError('Username already taken')
if (target.includes('mail')) throw new ActionError('Email already registered')
throw new ActionError('Username or email already in use')
}
throw err;
throw err
}
},
);
)
const updateUserInput = updateUserSchema.extend({
id: z.coerce.number().int().positive(),
});
})
export const updateUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: updateUserInput },
async (ctx) => {
const { id, diamonds, duckets, ...userData } = ctx.data;
const { id, diamonds, duckets, ...userData } = ctx.data
const targetUser = await guardRank(id, ctx.session.user.rank);
const targetUser = await guardRank(id, ctx.session.user.rank)
if (userData.rank !== undefined && userData.rank >= ctx.session.user.rank && ctx.session.user.rank < 7) {
throw new ActionError("Cannot assign rank equal or higher than your own");
if (
userData.rank !== undefined &&
userData.rank >= ctx.session.user.rank &&
ctx.session.user.rank < 7
) {
throw new ActionError('Cannot assign rank equal or higher than your own')
}
await prisma.user.update({ where: { id }, data: userData });
await prisma.user.update({ where: { id }, data: userData })
if (diamonds !== undefined) {
await prisma.usersCurrency.upsert({
where: { userId_type: { userId: id, type: 5 } },
update: { amount: diamonds },
create: { userId: id, type: 5, amount: diamonds },
});
})
}
if (duckets !== undefined) {
await prisma.usersCurrency.upsert({
where: { userId_type: { userId: id, type: 0 } },
update: { amount: duckets },
create: { userId: id, type: 0, amount: duckets },
});
})
}
logAudit({
userId: ctx.session.user.id,
action: "user_edit",
target: "User",
action: 'user_edit',
target: 'User',
targetId: id,
before: { username: targetUser.username, mail: targetUser.mail, rank: targetUser.rank },
after: userData,
});
})
notify({
action: "user_edit",
action: 'user_edit',
actor: ctx.session.user.username,
target: targetUser.username,
targetId: id,
});
})
return actionOk();
return actionOk()
},
);
)
const banInput = banUserSchema.extend({});
const banInput = banUserSchema.extend({})
export const banUser = adminAction({ permission: PERMS.USERS_BAN, schema: banInput }, async (ctx) => {
const { userId, reason, duration, type, ip } = ctx.data;
export const banUser = adminAction(
{ permission: PERMS.USERS_BAN, schema: banInput },
async (ctx) => {
const { userId, reason, duration, type, ip } = ctx.data
const targetUser = await guardRank(userId, ctx.session.user.rank);
const targetUser = await guardRank(userId, ctx.session.user.rank)
const now = Math.floor(Date.now() / 1000);
const banExpire = duration > 0 ? now + duration * 3600 : 0;
const now = Math.floor(Date.now() / 1000)
const banExpire = duration > 0 ? now + duration * 3600 : 0
await prisma.ban.create({
data: {
userId,
userStaffId: ctx.session.user.id,
timestamp: now,
banExpire,
banReason: reason,
type: type || "account",
ip: ip || "",
machineId: "",
},
});
await prisma.ban.create({
data: {
userId,
userStaffId: ctx.session.user.id,
timestamp: now,
banExpire,
banReason: reason,
type: type || 'account',
ip: ip || '',
machineId: '',
},
})
await rcon.disconnectUser(userId);
await rcon.disconnectUser(userId)
logAudit({
userId: ctx.session.user.id,
action: "ban",
target: "User",
targetId: userId,
after: { reason, type, duration },
});
logAudit({
userId: ctx.session.user.id,
action: 'ban',
target: 'User',
targetId: userId,
after: { reason, type, duration },
})
notify({
action: "ban",
actor: ctx.session.user.username,
target: targetUser.username,
details: reason,
});
notify({
action: 'ban',
actor: ctx.session.user.username,
target: targetUser.username,
details: reason,
})
return actionOk();
});
return actionOk()
},
)
const unbanInput = z.object({ userId: z.coerce.number().int().positive() });
const unbanInput = z.object({ userId: z.coerce.number().int().positive() })
export const unbanUser = adminAction({ permission: PERMS.USERS_BAN, schema: unbanInput }, async (ctx) => {
const { userId } = ctx.data;
export const unbanUser = adminAction(
{ permission: PERMS.USERS_BAN, schema: unbanInput },
async (ctx) => {
const { userId } = ctx.data
const targetUser = await guardRank(userId, ctx.session.user.rank);
const targetUser = await guardRank(userId, ctx.session.user.rank)
await prisma.ban.deleteMany({ where: { userId } });
await prisma.ban.deleteMany({ where: { userId } })
logAudit({
userId: ctx.session.user.id,
action: "unban",
target: "User",
targetId: userId,
});
logAudit({
userId: ctx.session.user.id,
action: 'unban',
target: 'User',
targetId: userId,
})
notify({
action: "unban",
actor: ctx.session.user.username,
target: targetUser.username,
});
notify({
action: 'unban',
actor: ctx.session.user.username,
target: targetUser.username,
})
return actionOk();
});
return actionOk()
},
)
export const giveBadge = adminAction(
{ permission: PERMS.USERS_EDIT, schema: giveBadgeSchema },
async (ctx) => {
const { userId, badgeCode } = ctx.data;
const { userId, badgeCode } = ctx.data
await guardRank(userId, ctx.session.user.rank);
await guardRank(userId, ctx.session.user.rank)
const existing = await prisma.usersBadges.findFirst({ where: { userId, badgeCode } });
if (existing) throw new ActionError("Badge already assigned");
const existing = await prisma.usersBadges.findFirst({ where: { userId, badgeCode } })
if (existing) throw new ActionError('Badge already assigned')
await prisma.usersBadges.create({ data: { userId, badgeCode } });
await rcon.giveBadge(userId, badgeCode);
await prisma.usersBadges.create({ data: { userId, badgeCode } })
await rcon.giveBadge(userId, badgeCode)
return actionOk();
return actionOk()
},
);
)
// ── Remove Badge ────────────────────────────────────────────────────
const removeBadgeSchema = z.object({
userId: z.coerce.number().int().positive(),
badgeCode: z.string().min(1),
});
})
export const removeBadge = adminAction(
{ permission: PERMS.USERS_EDIT, schema: removeBadgeSchema },
async (ctx) => {
const { userId, badgeCode } = ctx.data;
const { userId, badgeCode } = ctx.data
await guardRank(userId, ctx.session.user.rank);
await guardRank(userId, ctx.session.user.rank)
const existing = await prisma.usersBadges.findFirst({ where: { userId, badgeCode } });
if (!existing) throw new ActionError("Badge not found");
const existing = await prisma.usersBadges.findFirst({ where: { userId, badgeCode } })
if (!existing) throw new ActionError('Badge not found')
await prisma.usersBadges.delete({ where: { id: existing.id } });
await rcon.removeBadge(userId, badgeCode);
await prisma.usersBadges.delete({ where: { id: existing.id } })
await rcon.removeBadge(userId, badgeCode)
return actionOk();
return actionOk()
},
);
)
// ── Rank guard helper ───────────────────────────────────────────────
@@ -252,163 +267,169 @@ async function guardRank(targetUserId: number, sessionRank: number) {
const target = await prisma.user.findUnique({
where: { id: targetUserId },
select: { username: true, rank: true, mail: true },
});
if (!target) throw new ActionError("User not found");
})
if (!target) throw new ActionError('User not found')
if (target.rank >= sessionRank && sessionRank < 7) {
throw new ActionError("Cannot modify user with equal or higher rank");
throw new ActionError('Cannot modify user with equal or higher rank')
}
return target;
return target
}
// ── Reset Password ──────────────────────────────────────────────────
const resetPasswordSchema = z.object({
userId: z.coerce.number().int().positive(),
});
})
export const resetPassword = adminAction(
{ permission: PERMS.USERS_RESET_PASSWORD, schema: resetPasswordSchema },
async (ctx) => {
const target = await guardRank(ctx.data.userId, ctx.session.user.rank);
const target = await guardRank(ctx.data.userId, ctx.session.user.rank)
const newPassword = crypto.randomBytes(12).toString("base64url").slice(0, 16);
const hashed = await hash(newPassword, 10);
const newPassword = crypto.randomBytes(12).toString('base64url').slice(0, 16)
const hashed = await hash(newPassword, 10)
await prisma.user.update({
where: { id: ctx.data.userId },
data: { password: hashed },
});
})
logAudit({
userId: ctx.session.user.id,
action: "reset_password",
target: "User",
action: 'reset_password',
target: 'User',
targetId: ctx.data.userId,
});
})
notify({
action: "user_edit",
action: 'user_edit',
actor: ctx.session.user.username,
target: target.username,
details: "Password reset",
});
details: 'Password reset',
})
return actionOk({ newPassword });
return actionOk({ newPassword })
},
);
)
// ── Disconnect User ─────────────────────────────────────────────────
const disconnectSchema = z.object({
userId: z.coerce.number().int().positive(),
});
})
export const disconnectUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: disconnectSchema },
async (ctx) => {
const target = await guardRank(ctx.data.userId, ctx.session.user.rank);
const success = await rcon.disconnectUser(ctx.data.userId);
if (!success) throw new ActionError("Failed to disconnect. Is the emulator running?");
const target = await guardRank(ctx.data.userId, ctx.session.user.rank)
const success = await rcon.disconnectUser(ctx.data.userId)
if (!success) throw new ActionError('Failed to disconnect. Is the emulator running?')
logAudit({
userId: ctx.session.user.id,
action: "user_disconnect",
target: "User",
action: 'user_disconnect',
target: 'User',
targetId: ctx.data.userId,
});
})
notify({
action: "disconnect",
action: 'disconnect',
actor: ctx.session.user.username,
target: target.username,
});
})
return actionOk();
return actionOk()
},
);
)
// ── Alert User (in-game message) ────────────────────────────────────
const alertUserSchema = z.object({
userId: z.coerce.number().int().positive(),
message: z.string().min(1).max(500),
});
})
export const alertUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: alertUserSchema },
async (ctx) => {
const success = await rcon.alertUser(ctx.data.userId, ctx.data.message);
if (!success) throw new ActionError("Failed to send alert. Is the emulator running?");
return actionOk();
const success = await rcon.alertUser(ctx.data.userId, ctx.data.message)
if (!success) throw new ActionError('Failed to send alert. Is the emulator running?')
return actionOk()
},
);
)
// ── Mute User ───────────────────────────────────────────────────────
const muteSchema = z.object({
userId: z.coerce.number().int().positive(),
duration: z.coerce.number().int().min(0).default(0),
});
})
export const muteUser = adminAction({ permission: PERMS.USERS_EDIT, schema: muteSchema }, async (ctx) => {
const _target = await guardRank(ctx.data.userId, ctx.session.user.rank);
const success = await rcon.muteUser(ctx.data.userId, ctx.data.duration);
if (!success) throw new ActionError("Failed to mute. Is the emulator running?");
export const muteUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: muteSchema },
async (ctx) => {
const _target = await guardRank(ctx.data.userId, ctx.session.user.rank)
const success = await rcon.muteUser(ctx.data.userId, ctx.data.duration)
if (!success) throw new ActionError('Failed to mute. Is the emulator running?')
logAudit({
userId: ctx.session.user.id,
action: "user_mute",
target: "User",
targetId: ctx.data.userId,
after: { duration: ctx.data.duration },
});
logAudit({
userId: ctx.session.user.id,
action: 'user_mute',
target: 'User',
targetId: ctx.data.userId,
after: { duration: ctx.data.duration },
})
return actionOk();
});
return actionOk()
},
)
// ── Unmute User ─────────────────────────────────────────────────────
const unmuteSchema = z.object({
userId: z.coerce.number().int().positive(),
});
})
export const unmuteUser = adminAction({ permission: PERMS.USERS_EDIT, schema: unmuteSchema }, async (ctx) => {
await guardRank(ctx.data.userId, ctx.session.user.rank);
const success = await rcon.unmuteUser(ctx.data.userId);
if (!success) throw new ActionError("Failed to unmute. Is the emulator running?");
export const unmuteUser = adminAction(
{ permission: PERMS.USERS_EDIT, schema: unmuteSchema },
async (ctx) => {
await guardRank(ctx.data.userId, ctx.session.user.rank)
const success = await rcon.unmuteUser(ctx.data.userId)
if (!success) throw new ActionError('Failed to unmute. Is the emulator running?')
logAudit({
userId: ctx.session.user.id,
action: "user_unmute",
target: "User",
targetId: ctx.data.userId,
});
logAudit({
userId: ctx.session.user.id,
action: 'user_unmute',
target: 'User',
targetId: ctx.data.userId,
})
return actionOk();
});
return actionOk()
},
)
// ── Send Credits via RCON ───────────────────────────────────────────
const sendCreditsSchema = z.object({
userId: z.coerce.number().int().positive(),
amount: z.coerce.number().int().min(1).max(1000000),
});
})
export const sendCredits = adminAction(
{ permission: PERMS.USERS_EDIT, schema: sendCreditsSchema },
async (ctx) => {
const _target = await guardRank(ctx.data.userId, ctx.session.user.rank);
const success = await rcon.giveCredits(ctx.data.userId, ctx.data.amount);
if (!success) throw new ActionError("Failed to send credits. Is the emulator running?");
const _target = await guardRank(ctx.data.userId, ctx.session.user.rank)
const success = await rcon.giveCredits(ctx.data.userId, ctx.data.amount)
if (!success) throw new ActionError('Failed to send credits. Is the emulator running?')
logAudit({
userId: ctx.session.user.id,
action: "user_send_credits",
target: "User",
action: 'user_send_credits',
target: 'User',
targetId: ctx.data.userId,
after: { amount: ctx.data.amount },
});
})
return actionOk();
return actionOk()
},
);
)
+19 -19
View File
@@ -1,16 +1,16 @@
"use server";
'use server'
import { revalidateTag } from "next/cache";
import { z } from "zod";
import { PERMS } from "@/lib/permission-slugs";
import { prisma } from "@/lib/prisma";
import { adminAction } from "@/lib/safe-action";
import { actionOk } from "@/lib/safe-action-shared";
import { revalidateTag } from 'next/cache'
import { z } from 'zod'
import { PERMS } from '@/lib/permission-slugs'
import { prisma } from '@/lib/prisma'
import { adminAction } from '@/lib/safe-action'
import { actionOk } from '@/lib/safe-action-shared'
const toggleWatchSchema = z.object({
targetUserId: z.coerce.number().int().positive(),
reason: z.string().max(255).optional(),
});
})
/**
* Toggle a watch entry for the calling staff on the given target user.
@@ -21,23 +21,23 @@ const toggleWatchSchema = z.object({
export const toggleUserWatch = adminAction(
{ permission: PERMS.USERS_VIEW, schema: toggleWatchSchema },
async (ctx) => {
const staffId = ctx.session.user.id;
const { targetUserId, reason } = ctx.data;
const staffId = ctx.session.user.id
const { targetUserId, reason } = ctx.data
const existing = await prisma.userWatch.findUnique({
where: { staffId_targetUserId: { staffId, targetUserId } },
});
})
if (existing) {
await prisma.userWatch.delete({ where: { id: existing.id } });
revalidateTag(`user-watch:${staffId}`, { expire: 0 });
return actionOk({ watching: false });
await prisma.userWatch.delete({ where: { id: existing.id } })
revalidateTag(`user-watch:${staffId}`, { expire: 0 })
return actionOk({ watching: false })
}
await prisma.userWatch.create({
data: { staffId, targetUserId, reason: reason ?? "" },
});
revalidateTag(`user-watch:${staffId}`, { expire: 0 });
return actionOk({ watching: true });
data: { staffId, targetUserId, reason: reason ?? '' },
})
revalidateTag(`user-watch:${staffId}`, { expire: 0 })
return actionOk({ watching: true })
},
);
)