diff --git a/prisma/migrations/0007_login_logs.sql b/prisma/migrations/0007_login_logs.sql new file mode 100644 index 0000000000..da50360bb2 --- /dev/null +++ b/prisma/migrations/0007_login_logs.sql @@ -0,0 +1,11 @@ +-- Successful-login history (the AtomCMS "session logs" equivalent; our auth is +-- stateless JWT so there's no sessions table). Idempotent (MariaDB). +CREATE TABLE IF NOT EXISTS website_login_logs ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + user_id INT NOT NULL, + ip VARCHAR(45) NOT NULL, + user_agent VARCHAR(512) NULL, + created_at TIMESTAMP NULL, + PRIMARY KEY (id), + KEY website_login_logs_user_id_idx (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3c64cc350a..fa293ad706 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1687,6 +1687,17 @@ model WebsiteWriteableBoxes { @@map("website_writeable_boxes") } +model WebsiteLoginLogs { + id BigInt @id @default(autoincrement()) @db.UnsignedBigInt + userId Int @map("user_id") + ip String @db.VarChar(45) + userAgent String? @map("user_agent") @db.VarChar(512) + createdAt DateTime? @map("created_at") @db.Timestamp(0) + + @@index([userId]) + @@map("website_login_logs") +} + model WebsiteBetaCodes { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt code String @unique @db.VarChar(255) diff --git a/src/actions/admin-radio-moderation.ts b/src/actions/admin-radio-moderation.ts new file mode 100644 index 0000000000..631e337ffe --- /dev/null +++ b/src/actions/admin-radio-moderation.ts @@ -0,0 +1,43 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { requireStaff } from '@/lib/admin/guard'; +import { logStaffActivity } from '@/lib/services/staff-activity'; +import { prisma } from '@/lib/prisma'; + +/** Parse a FormData field into a positive BigInt id, or null when invalid. */ +function parseId(raw: FormDataEntryValue | null): bigint | null { + if (typeof raw !== 'string' || raw.trim() === '') return null; + try { + const id = BigInt(raw.trim()); + return id > 0n ? id : null; + } catch { + return null; + } +} + +/** + * Delete a radio shout from the DJ moderation page. Re-reads auth via + * requireStaff, writes a staff-activity audit entry and revalidates the + * moderation route. Fails soft if the row is already gone. + */ +export async function deleteShout(formData: FormData): Promise { + const staff = await requireStaff(); + const id = parseId(formData.get('id')); + if (id === null) return; + + try { + await prisma.radioShouts.delete({ where: { id } }); + await logStaffActivity({ + staffId: staff.id, + action: 'radio.shout.delete', + description: `Deleted radio shout #${id}`, + targetType: 'radio_shout', + targetId: Number(id), + }); + } catch { + // Row may already be gone; ignore so the action does not throw. + } + + revalidatePath('/admin/radio/moderation'); +} diff --git a/src/app/admin/articles/[id]/page.tsx b/src/app/admin/articles/[id]/page.tsx index 07c5d5f9d8..55ca875890 100644 --- a/src/app/admin/articles/[id]/page.tsx +++ b/src/app/admin/articles/[id]/page.tsx @@ -1,6 +1,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { deleteArticle, updateArticle } from "@/actions/admin-articles"; +import { RichText } from "@/components/admin/rich-text"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; @@ -30,7 +31,7 @@ export default async function EditArticle({