diff --git a/.env.example b/.env.example deleted file mode 100644 index c7568ff8..00000000 --- a/.env.example +++ /dev/null @@ -1,65 +0,0 @@ -# Connection to the LIVE/COPY emulator MySQL/MariaDB database. -# The schema is owned by the Arcturus emulator — this app reads/writes data, -# it does NOT own or migrate the emulator tables. Format: -DATABASE_URL=mysql://user:password@127.0.0.1:3306/atomcms - -# Optional pool tuning (defaults shown) -DATABASE_POOL_SIZE=40 -DATABASE_IDLE_TIMEOUT_MS=300000 -DATABASE_CONNECT_TIMEOUT_MS=10000 - -# Used by SSO ticket generation ({HOTEL_NAME}-{uuid}) -HOTEL_NAME=Atom -APP_URL=http://localhost:3000 - -# NextAuth (>=32 chars) + Laravel APP_KEY (base64:...) for existing 2FA secrets -AUTH_SECRET= -APP_KEY= -CONVERT_PASSWORDS=false - -# Password hashing for NEW/upgraded passwords: "bcrypt" (default; 60-char $2y$, -# fits a varchar(64) users.password) or "argon2id" (~97 chars, needs a wider -# column). Existing accounts in either format still verify on login. -PASSWORD_HASH=bcrypt - -# Filesystem directory the badge uploader (/admin/badges) writes .gif into -# — the emulator's badge image folder (e.g. .../assets/c_images/album1584). -# Leave unset to disable badge uploads. -BADGE_UPLOAD_DIR= - -# Emulator JAR backup job (jobs-worker, runs host-side). When both are set, the -# worker copies the JAR daily into the backup dir, keeping the newest N. -EMULATOR_JAR_PATH= -EMULATOR_BACKUP_DIR= -EMULATOR_BACKUP_KEEP=7 - -# RCON link to the Arcturus emulator -RCON_HOST=127.0.0.1 -RCON_PORT=3001 - -# Optional OAuth (enabled when both id+secret are set) -DISCORD_CLIENT_ID= -DISCORD_CLIENT_SECRET= -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= - -# Optional SMTP (password reset / alert emails) -SMTP_HOST= -SMTP_PORT=587 -SMTP_USER= -SMTP_PASSWORD= -SMTP_FROM= - -# Optional alerting (jobs worker / alert service) -DISCORD_WEBHOOK_URL= -ALERT_EMAIL= - -# Optional AI content moderation (user comments / guestbook). -# When set, posts are checked against the OpenAI Moderations endpoint in -# addition to the website_wordfilter blocklist. Fail-open if unset/erroring. -OPENAI_API_KEY= - -# Optional PayPal top-up (sandbox by default) -PAYPAL_CLIENT_ID= -PAYPAL_SECRET= -PAYPAL_API=https://api-m.sandbox.paypal.com diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fa293ad7..b3952ce2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1564,7 +1564,7 @@ model WebsiteArticles { title String @db.VarChar(255) shortStory String @map("short_story") @db.VarChar(255) fullStory String @map("full_story") @db.Text - userId String @map("user_id") @db.VarChar(255) + userId Int? @map("user_id") image String @db.VarChar(255) createdAt DateTime? @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) diff --git a/public/assets/images/EnterHubbly.png b/public/assets/images/EnterHubbly.png new file mode 100644 index 00000000..e62ffa78 Binary files /dev/null and b/public/assets/images/EnterHubbly.png differ diff --git a/public/assets/images/FrankwithBag.gif b/public/assets/images/FrankwithBag.gif new file mode 100755 index 00000000..513da27e Binary files /dev/null and b/public/assets/images/FrankwithBag.gif differ diff --git a/public/assets/images/angry_frank.png b/public/assets/images/angry_frank.png new file mode 100755 index 00000000..96d774d5 Binary files /dev/null and b/public/assets/images/angry_frank.png differ diff --git a/public/assets/images/icons/navigation/bb.png b/public/assets/images/icons/navigation/bb.png new file mode 100644 index 00000000..f401d735 Binary files /dev/null and b/public/assets/images/icons/navigation/bb.png differ diff --git a/public/assets/images/icons/navigation/community.png b/public/assets/images/icons/navigation/community.png index 8483fa04..dffcabd0 100644 Binary files a/public/assets/images/icons/navigation/community.png and b/public/assets/images/icons/navigation/community.png differ diff --git a/public/assets/images/icons/navigation/goody.png b/public/assets/images/icons/navigation/goody.png new file mode 100644 index 00000000..73617601 Binary files /dev/null and b/public/assets/images/icons/navigation/goody.png differ diff --git a/public/assets/images/icons/navigation/leaderboards.png b/public/assets/images/icons/navigation/leaderboards.png index d1ed5165..ef416e1f 100644 Binary files a/public/assets/images/icons/navigation/leaderboards.png and b/public/assets/images/icons/navigation/leaderboards.png differ diff --git a/public/assets/images/icons/navigation/me.png b/public/assets/images/icons/navigation/me.png new file mode 100644 index 00000000..a97ab397 Binary files /dev/null and b/public/assets/images/icons/navigation/me.png differ diff --git a/public/assets/images/icons/navigation/shop.png b/public/assets/images/icons/navigation/shop.png index 65f3abd6..5493a848 100644 Binary files a/public/assets/images/icons/navigation/shop.png and b/public/assets/images/icons/navigation/shop.png differ diff --git a/public/assets/images/kasja_mepage_header.png b/public/assets/images/kasja_mepage_header.png new file mode 100755 index 00000000..bd8c10f7 Binary files /dev/null and b/public/assets/images/kasja_mepage_header.png differ diff --git a/public/assets/images/mybobba_banner.png b/public/assets/images/mybobba_banner.png new file mode 100644 index 00000000..93f56f7c Binary files /dev/null and b/public/assets/images/mybobba_banner.png differ diff --git a/public/assets/images/user-online.png b/public/assets/images/user-online.png new file mode 100644 index 00000000..f10addd4 Binary files /dev/null and b/public/assets/images/user-online.png differ diff --git a/src/actions/admin-articles.ts b/src/actions/admin-articles.ts index 9d3f2394..45259e92 100644 --- a/src/actions/admin-articles.ts +++ b/src/actions/admin-articles.ts @@ -10,7 +10,7 @@ async function uniqueSlug(title: string): Promise { const base = slugify(title); let slug = base; let n = 2; - while (await prisma.websiteArticles.findUnique({ where: { slug } })) { + while (await prisma.websiteArticles.findUnique({ where: { slug }, select: { id: true } })) { slug = `${base}-${n++}`; } return slug; @@ -32,7 +32,7 @@ export async function createArticle(formData: FormData): Promise { shortStory: shortStory.slice(0, 255), fullStory, image: image.slice(0, 255), - userId: String(staff.id), + userId: staff.id, createdAt: now, updatedAt: now, }, @@ -59,6 +59,9 @@ export async function updateArticle(formData: FormData): Promise { export async function deleteArticle(formData: FormData): Promise { await requireStaff(); - await prisma.websiteArticles.delete({ where: { id: BigInt(String(formData.get("id"))) } }); + const id = BigInt(String(formData.get("id"))); + await prisma.websiteArticleReactions.deleteMany({ where: { articleId: id } }); + await prisma.websiteArticleComments.deleteMany({ where: { articleId: id } }); + await prisma.websiteArticles.delete({ where: { id } }); redirect("/admin/articles"); } diff --git a/src/app/admin/articles/[id]/page.tsx b/src/app/admin/articles/[id]/page.tsx index 55ca8758..e7411be2 100644 --- a/src/app/admin/articles/[id]/page.tsx +++ b/src/app/admin/articles/[id]/page.tsx @@ -12,12 +12,10 @@ export default async function EditArticle({ params: Promise<{ id: string }>; }) { const { id } = await params; - let article: Awaited> = null; - try { - article = await prisma.websiteArticles.findUnique({ where: { id: BigInt(id) } }); - } catch { - notFound(); - } + const article = await prisma.websiteArticles.findUnique({ + where: { id: BigInt(id) }, + select: { id: true, slug: true, title: true, shortStory: true, fullStory: true, image: true, createdAt: true, updatedAt: true }, + }).catch(() => null); if (!article) notFound(); return ( @@ -36,7 +34,7 @@ export default async function EditArticle({
- +
); diff --git a/src/app/admin/articles/page.tsx b/src/app/admin/articles/page.tsx index edaa32ae..59cebda7 100644 --- a/src/app/admin/articles/page.tsx +++ b/src/app/admin/articles/page.tsx @@ -1,12 +1,11 @@ import Link from "next/link"; import { StatusCard } from "@/components/admin/dashboard"; +import { deleteArticle } from "@/actions/admin-articles"; import { prisma } from "@/lib/prisma"; export const dynamic = "force-dynamic"; export default async function AdminArticles() { - // .catch(() => []) keeps the precise selected-row type while falling back to - // an empty list when the DB is unavailable (instead of a 500). const articles = await prisma.websiteArticles .findMany({ orderBy: { createdAt: "desc" }, @@ -47,13 +46,16 @@ export default async function AdminArticles() { Title Slug Created + Actions {articles.map((a) => ( - {a.title} + + {a.title} + {a.slug} @@ -61,6 +63,23 @@ export default async function AdminArticles() { {a.createdAt ? a.createdAt.toISOString().slice(0, 10) : "—"} + +
+ + Edit + +
+ + +
+
+ ))} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 254ce0d9..2bbe8801 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -58,20 +58,20 @@ export default async function AdminDashboard() { value={online} hint={dbOk ? `${users} accounts` : "DB unavailable"} state={dbOk ? "neutral" : "danger"} - icon="\u{1F464}" + icon="👤" /> - + 0 ? "warn" : "neutral"} - icon="\u{1F528}" + icon="🔧" /> diff --git a/src/app/api/client/sso/route.ts b/src/app/api/client/sso/route.ts new file mode 100644 index 00000000..9e189ddf --- /dev/null +++ b/src/app/api/client/sso/route.ts @@ -0,0 +1,28 @@ +import { headers } from "next/headers"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; +import { siteSettings } from "@/lib/services/site-settings"; +import { issueSsoTicket } from "@/lib/auth/sso-ticket"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const session = await auth(); + if (!session?.user?.id) { + return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); + } + + const userId = Number(session.user.id); + const [hotelName, clientUrl] = await Promise.all([ + siteSettings.get("hotel_name", "Atom"), + siteSettings.get("nitro_client_url", ""), + ]); + + const hdrs = await headers(); + const ip = + hdrs.get("x-forwarded-for")?.split(",")[0]?.trim() ?? hdrs.get("x-real-ip") ?? "0.0.0.0"; + + const ticket = await issueSsoTicket(prisma, userId, hotelName ?? "Atom", ip); + + return Response.json({ ticket, hotelName: hotelName ?? "Atom", clientUrl }); +} \ No newline at end of file diff --git a/src/app/client/flash/page.tsx b/src/app/client/flash/page.tsx deleted file mode 100644 index af0aecb5..00000000 --- a/src/app/client/flash/page.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import Link from "next/link"; -import { headers } from "next/headers"; -import { redirect } from "next/navigation"; -import { ContentCard, EmptyState } from "@/components/public/ui"; -import { issueSsoTicket } from "@/lib/auth/sso-ticket"; -import { buildNitroClientUrl } from "@/lib/services/clients"; -import { auth } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; -import { siteSettings } from "@/lib/services/site-settings"; - -export const dynamic = "force-dynamic"; - -export default async function FlashClientPage() { - const session = await auth(); - if (!session?.user?.id) redirect("/login"); - - const userId = Number(session.user.id); - // The Flash client base URL may be stored under either settings key. - const [hotelNameRaw, flashUrl, flashUrlAlt] = await Promise.all([ - siteSettings.get("hotel_name", "Atom").catch(() => "Atom"), - siteSettings.get("flash_client_url", "").catch(() => ""), - siteSettings.get("client_flash_url", "").catch(() => ""), - ]); - const hotelName = hotelNameRaw ?? "Atom"; - - const hdrs = await headers(); - const ip = - hdrs.get("x-forwarded-for")?.split(",")[0]?.trim() ?? hdrs.get("x-real-ip") ?? "0.0.0.0"; - - // Faithful to AtomCMS's client controllers: issue (and persist) the SSO ticket - // on the launcher render, then hand it to the Flash client. - const ticket = await issueSsoTicket(prisma, userId, hotelName, ip); - - // Use a configured Flash base URL (with the SSO ticket appended); otherwise - // fall back to the Nitro client so the launcher still works. - const flashBase = flashUrl || flashUrlAlt || ""; - const flashSrc = flashBase - ? `${flashBase}${flashBase.includes("?") ? "&" : "?"}sso=${encodeURIComponent(ticket)}` - : ""; - const src = flashSrc || (await buildNitroClientUrl(ticket)); - - return ( -
- - Use Nitro client - - } - /> - - {src ? ( - <> - - Launch in a new tab - - } - /> - -