revert fb8e77bb68
Local Build and Deploy / deploy (push) Successful in 1m11s
Local Build and Deploy / deploy (push) Successful in 1m11s
revert style: clean up code with prettier and eslint
This commit is contained in:
1 parent
27de078f54
commit
e85e4d74ea
378 files changed
+20710
-22428
No files matched your search
@@ -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
@@ -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()
|
||||
},
|
||||
);
|
||||
)
|
||||
@@ -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 }> = [];
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 })
|
||||
},
|
||||
);
|
||||
)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 })
|
||||
},
|
||||
);
|
||||
)
|
||||
Reference in new issue
Block a user