diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3b4a0b2c..54b7f098 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1645,12 +1645,12 @@ model WebsiteIpBlacklist { } model WebsitePermissions { - id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - key String @unique @db.VarChar(255) - value String? @db.VarChar(255) - comment String? @db.VarChar(255) - createdAt DateTime? @map("created_at") @db.Timestamp(0) - updatedAt DateTime? @map("updated_at") @db.Timestamp(0) + id BigInt @id @default(autoincrement()) @db.UnsignedBigInt + permission String @unique @db.VarChar(255) + minRank Int @map("min_rank") + description String? @db.Text + createdAt DateTime? @map("created_at") @db.Timestamp(0) + updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@map("website_permissions") } @@ -1686,12 +1686,12 @@ model WebsiteBetaCodes { model WebsiteTeams { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - rankName String @unique @db.VarChar(255) - hiddenRank Boolean @default(false) + rankName String @unique @map("rank_name") @db.VarChar(255) + hiddenRank Boolean @default(false) @map("hidden_rank") badge String? @db.VarChar(255) - jobDescription String? @db.VarChar(255) - staffColor String @default("#327fa8") @db.VarChar(255) - staffBackground String @default("staff-bg.png") @db.VarChar(255) + jobDescription String? @map("job_description") @db.VarChar(255) + staffColor String @default("#327fa8") @map("staff_color") @db.VarChar(255) + staffBackground String @default("staff-bg.png") @map("staff_background") @db.VarChar(255) createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -1750,7 +1750,7 @@ model WebsiteRareValues { name String @db.VarChar(255) creditValue String? @map("credit_value") @db.VarChar(255) currencyValue String? @map("currency_value") @db.VarChar(255) - currencyType String @default("diamonds") @db.VarChar(255) + currencyType String @default("diamonds") @map("currency_type") @db.VarChar(255) furnitureIcon String @map("furniture_icon") @db.VarChar(255) createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -1809,7 +1809,7 @@ model WebsiteHelpCenterCategories { model WebsiteHelpCenterTickets { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - userId Int? + userId Int? @map("user_id") categoryId BigInt? @map("category_id") @db.UnsignedBigInt title String @db.VarChar(255) content String @db.Text @@ -1822,7 +1822,7 @@ model WebsiteHelpCenterTickets { model WebsitePaypalTransactions { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - userId Int + userId Int @map("user_id") transactionId String @map("transaction_id") @db.VarChar(255) status String? @db.VarChar(255) description String? @map("description") @db.VarChar(512) @@ -1836,12 +1836,14 @@ model WebsitePaypalTransactions { model WebsiteShopArticles { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt + categoryId BigInt? @map("website_shop_category_id") @db.UnsignedBigInt name String @unique @db.VarChar(255) info String @db.VarChar(255) - icon String @db.VarChar(255) + iconUrl String @map("icon_url") @db.VarChar(255) color String @db.VarChar(255) costs Int @db.UnsignedInt giveRank Int? @map("give_rank") + isGiftable Boolean @default(false) @map("is_giftable") credits Int? @db.UnsignedInt duckets Int? @db.UnsignedInt diamonds Int? @db.UnsignedInt @@ -1869,7 +1871,7 @@ model WebsiteShopVouchers { model WebsiteUsedShopVouchers { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - userId Int + userId Int @map("user_id") voucherId BigInt @map("voucher_id") @db.UnsignedBigInt createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -1880,7 +1882,7 @@ model WebsiteUsedShopVouchers { model WebsiteShopArticleFeatures { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt articleId BigInt @map("article_id") @db.UnsignedBigInt - features Json + content String @db.Text createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -1890,7 +1892,7 @@ model WebsiteShopArticleFeatures { model WebsiteHelpCenterTicketReplies { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt ticketId BigInt @map("ticket_id") @db.UnsignedBigInt - userId Int + userId Int @map("user_id") content String @db.Text createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -1917,7 +1919,7 @@ model AlertLogs { model WebsiteUserGuestbooks { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt profileId Int @map("profile_id") - userId Int + userId Int @map("user_id") message String @db.VarChar(255) createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -1926,19 +1928,18 @@ model WebsiteUserGuestbooks { } model WebsiteShopCategories { - id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - name String @db.VarChar(255) - slug String @unique @db.VarChar(255) - icon String? @db.VarChar(255) - createdAt DateTime? @map("created_at") @db.Timestamp(0) - updatedAt DateTime? @map("updated_at") @db.Timestamp(0) + id BigInt @id @default(autoincrement()) @db.UnsignedBigInt + name String @db.VarChar(255) + description String? @db.Text + icon String? @db.VarChar(255) + order Int @default(0) @@map("website_shop_categories") } model WebsiteMaintenanceTasks { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt - userId Int? + userId Int? @map("user_id") task String @db.VarChar(255) completed Boolean @default(false) createdAt DateTime? @map("created_at") @db.Timestamp(0) diff --git a/src/actions/admin-permissions.ts b/src/actions/admin-permissions.ts index 54d383c8..bb86d96c 100644 --- a/src/actions/admin-permissions.ts +++ b/src/actions/admin-permissions.ts @@ -5,36 +5,34 @@ import { requireStaff } from "@/lib/admin/guard"; import { prisma } from "@/lib/prisma"; import { logStaffActivity } from "@/lib/services/staff-activity"; -// website_permissions (model WebsitePermissions) is the CMS-owned rank-permission -// mapping AtomCMS exposes in admin: a key/value(/comment) row per permission. -// Editable columns on the table are exactly: key (unique), value, comment. -// id is BigInt and created_at/updated_at are managed here — no other columns -// exist, so there are no extra staff flags to toggle. +// website_permissions (model WebsitePermissions) is the CMS-owned permission -> +// minimum-rank mapping. Editable columns are exactly: permission (unique name), +// min_rank (lowest rank that may use it), description. id is BigInt and +// created_at/updated_at are managed here. + +function parseMinRank(formData: FormData): number { + const n = Number(String(formData.get("minRank") ?? "").trim()); + return Number.isInteger(n) && n >= 0 ? n : 1; +} export async function createPermission(formData: FormData): Promise { const staff = await requireStaff(); - const key = String(formData.get("key") ?? "").trim().slice(0, 255); - const value = String(formData.get("value") ?? "").trim().slice(0, 255); - const comment = String(formData.get("comment") ?? "").trim().slice(0, 255); - if (!key) return; + const permission = String(formData.get("permission") ?? "").trim().slice(0, 255); + const minRank = parseMinRank(formData); + const description = String(formData.get("description") ?? "").trim() || null; + if (!permission) return; const now = new Date(); try { await prisma.websitePermissions.upsert({ - where: { key }, - update: { value: value || null, comment: comment || null, updatedAt: now }, - create: { - key, - value: value || null, - comment: comment || null, - createdAt: now, - updatedAt: now, - }, + where: { permission }, + update: { minRank, description, updatedAt: now }, + create: { permission, minRank, description, createdAt: now, updatedAt: now }, }); await logStaffActivity({ staffId: staff.id, action: "permission_create", - description: `Saved permission "${key}" = "${value}"`, + description: `Saved permission "${permission}" (min rank ${minRank})`, targetType: "permission", }); } catch { @@ -48,29 +46,24 @@ export async function updatePermission(formData: FormData): Promise { const raw = String(formData.get("id") ?? ""); if (!raw) return; const id = BigInt(raw); - const key = String(formData.get("key") ?? "").trim().slice(0, 255); - const value = String(formData.get("value") ?? "").trim().slice(0, 255); - const comment = String(formData.get("comment") ?? "").trim().slice(0, 255); - if (!key) return; + const permission = String(formData.get("permission") ?? "").trim().slice(0, 255); + const minRank = parseMinRank(formData); + const description = String(formData.get("description") ?? "").trim() || null; + if (!permission) return; try { await prisma.websitePermissions.update({ where: { id }, - data: { - key, - value: value || null, - comment: comment || null, - updatedAt: new Date(), - }, + data: { permission, minRank, description, updatedAt: new Date() }, }); await logStaffActivity({ staffId: staff.id, action: "permission_update", - description: `Updated permission #${raw} ("${key}" = "${value}")`, + description: `Updated permission #${raw} ("${permission}" min rank ${minRank})`, targetType: "permission", }); } catch { - // ignore (e.g. duplicate key) — page re-renders current state + // ignore (e.g. duplicate) — page re-renders current state } revalidatePath("/admin/permissions"); } @@ -83,12 +76,12 @@ export async function deletePermission(formData: FormData): Promise { try { const deleted = await prisma.websitePermissions.delete({ where: { id: BigInt(raw) }, - select: { key: true }, + select: { permission: true }, }); await logStaffActivity({ staffId: staff.id, action: "permission_delete", - description: `Deleted permission #${raw} ("${deleted.key}")`, + description: `Deleted permission #${raw} ("${deleted.permission}")`, targetType: "permission", }); } catch { diff --git a/src/actions/admin-shop.ts b/src/actions/admin-shop.ts index 8394579f..66f5863d 100644 --- a/src/actions/admin-shop.ts +++ b/src/actions/admin-shop.ts @@ -37,7 +37,7 @@ export async function createShopArticle(formData: FormData): Promise { data: { name, info: String(formData.get("info") ?? "").trim().slice(0, 255), - icon: String(formData.get("icon") ?? "").trim().slice(0, 255), + iconUrl: String(formData.get("icon") ?? "").trim().slice(0, 255), color: String(formData.get("color") ?? "").trim().slice(0, 255), costs: reqUInt(formData, "costs"), giveRank: optUInt(formData, "giveRank"), @@ -86,7 +86,7 @@ export async function updateShopArticle(formData: FormData): Promise { data: { name, info: String(formData.get("info") ?? "").trim().slice(0, 255), - icon: String(formData.get("icon") ?? "").trim().slice(0, 255), + iconUrl: String(formData.get("icon") ?? "").trim().slice(0, 255), color: String(formData.get("color") ?? "").trim().slice(0, 255), costs: reqUInt(formData, "costs"), giveRank: optUInt(formData, "giveRank"), diff --git a/src/app/admin/permissions/page.tsx b/src/app/admin/permissions/page.tsx index 2fa3d1b1..d6190ed0 100644 --- a/src/app/admin/permissions/page.tsx +++ b/src/app/admin/permissions/page.tsx @@ -8,21 +8,21 @@ import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; // Manages website_permissions (model WebsitePermissions) — the CMS-owned -// rank-permission mapping AtomCMS exposes in admin. Each row is a key with an -// optional value and comment; those are the only editable columns on the table. +// permission -> minimum-rank mapping. Each row names a permission and the lowest +// rank allowed to use it, with an optional description. type PermissionRow = { id: bigint; - key: string; - value: string | null; - comment: string | null; + permission: string; + minRank: number; + description: string | null; }; export default async function AdminPermissions() { let permissions: PermissionRow[] = []; try { permissions = await prisma.websitePermissions.findMany({ - select: { id: true, key: true, value: true, comment: true }, - orderBy: { key: "asc" }, + select: { id: true, permission: true, minRank: true, description: true }, + orderBy: { minRank: "asc" }, take: 500, }); } catch { @@ -33,21 +33,27 @@ export default async function AdminPermissions() {

Permissions

- CMS-owned rank permissions (website_permissions). Each entry - maps a permission key to its value/name and an optional comment. + CMS-owned permissions (website_permissions). Each entry maps + a permission name to the minimum rank allowed to use it.

Add / overwrite permission

- + + - @@ -61,9 +67,9 @@ export default async function AdminPermissions() { ID - Key - Value - Comment + Permission + Min rank + Description @@ -78,24 +84,24 @@ export default async function AdminPermissions() { style={{ display: "contents" }} > - + diff --git a/src/app/admin/shop/[id]/page.tsx b/src/app/admin/shop/[id]/page.tsx index 5e740143..8da69a7a 100644 --- a/src/app/admin/shop/[id]/page.tsx +++ b/src/app/admin/shop/[id]/page.tsx @@ -29,7 +29,7 @@ export default async function EditShopArticle({ - + diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx index 9095f4ff..1a10aeae 100644 --- a/src/app/shop/page.tsx +++ b/src/app/shop/page.tsx @@ -15,7 +15,7 @@ type ShopArticle = { id: bigint; name: string; info: string; - icon: string; + iconUrl: string; color: string; costs: number; credits: number | null; @@ -28,7 +28,6 @@ type ShopArticle = { type ShopCategory = { id: bigint; name: string; - slug: string; icon: string | null; }; @@ -36,7 +35,7 @@ const ARTICLE_SELECT = { id: true, name: true, info: true, - icon: true, + iconUrl: true, color: true, costs: true, credits: true, @@ -59,7 +58,7 @@ export default async function ShopPage({ let categories: ShopCategory[] = []; try { categories = await prisma.websiteShopCategories.findMany({ - select: { id: true, name: true, slug: true, icon: true }, + select: { id: true, name: true, icon: true }, orderBy: { name: "asc" }, }); } catch { @@ -169,12 +168,12 @@ export default async function ShopPage({ overflow: "hidden", }} > - {a.icon ? ( + {a.iconUrl ? ( // Article icons are arbitrary external/Imgur URLs from the // emulator data, so a plain is correct here. // eslint-disable-next-line @next/next/no-img-element