From 4dfe698009a67ddfd50dade42e6f166bc95579fa Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 29 Jun 2026 18:26:34 +0200 Subject: [PATCH] Close remaining web gaps: rich profile, login history, lightbox/slider, flash client, admin chatlog/DJ/chart/WYSIWYG, niche API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rich profile (/u/[username]): wallet (credits/duckets/diamonds), friends grid (messenger_friendships), and owned rooms sections. - Login history: new website_login_logs table (model + migration 0007), recorded on every successful sign-in (ip + user-agent), surfaced on a new /settings/sessions page (with failed-attempt list from failed_logins). - Photos lightbox + home article slider (client components, no Swiper dep). - /client/flash launcher (SSO ticket like the Nitro page). - Admin: private chatlogs section in /admin/logs, /admin/radio/moderation (shout moderation), a "users by rank" inline bar chart on the dashboard, and a TinyMCE rich-text editor on the article admin forms. - Niche API: /api/values/[id], /api/guilds(+/[id]), /api/radio/auto-play. Verified live (prod, amx_test): login recorded โ†’ /settings/sessions shows it with device; profile renders wallet/friends/rooms; dashboard chart + private-chat logs + /client/flash + /api/guilds all OK. Reverted test data. tsc 0, vitest 49/49, next build 0. --- prisma/migrations/0007_login_logs.sql | 11 ++ prisma/schema.prisma | 11 ++ src/actions/admin-radio-moderation.ts | 43 ++++ src/app/admin/articles/[id]/page.tsx | 3 +- src/app/admin/articles/new/page.tsx | 3 +- src/app/admin/logs/page.tsx | 58 +++++- src/app/admin/page.tsx | 64 +++++- src/app/admin/radio/moderation/page.tsx | 96 +++++++++ src/app/api/guilds/[id]/route.ts | 55 ++++++ src/app/api/guilds/route.ts | 47 +++++ src/app/api/radio/auto-play/route.ts | 38 ++++ src/app/api/values/[id]/route.ts | 70 +++++++ src/app/client/flash/page.tsx | 99 ++++++++++ src/app/client/page.tsx | 6 + src/app/page.tsx | 37 ++-- src/app/photos/page.tsx | 40 ++-- src/app/settings/sessions/page.tsx | 123 ++++++++++++ src/app/u/[username]/page.tsx | 238 ++++++++++++++++++++++- src/components/admin/rich-text.tsx | 136 +++++++++++++ src/components/public/article-slider.tsx | 143 ++++++++++++++ src/components/public/photo-lightbox.tsx | 167 ++++++++++++++++ src/components/top-header.tsx | 3 + src/lib/auth.ts | 12 ++ src/messages/en.json | 12 +- src/messages/it.json | 12 +- 25 files changed, 1463 insertions(+), 64 deletions(-) create mode 100644 prisma/migrations/0007_login_logs.sql create mode 100644 src/actions/admin-radio-moderation.ts create mode 100644 src/app/admin/radio/moderation/page.tsx create mode 100644 src/app/api/guilds/[id]/route.ts create mode 100644 src/app/api/guilds/route.ts create mode 100644 src/app/api/radio/auto-play/route.ts create mode 100644 src/app/api/values/[id]/route.ts create mode 100644 src/app/client/flash/page.tsx create mode 100644 src/app/settings/sessions/page.tsx create mode 100644 src/components/admin/rich-text.tsx create mode 100644 src/components/public/article-slider.tsx create mode 100644 src/components/public/photo-lightbox.tsx 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({