86 lines
5.2 KiB
TypeScript
86 lines
5.2 KiB
TypeScript
import { appSettings } from "@minecraft-account-manager/database";
|
|
import { eq } from "drizzle-orm";
|
|
import { DEFAULT_ADMISSION_MESSAGES } from "@/lib/admission-settings";
|
|
import { db } from "@/lib/database";
|
|
import { saveAdmissionSettings } from "../actions";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function SettingsPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ saved?: string; error?: string }>;
|
|
}) {
|
|
const query = await searchParams;
|
|
const [settings] = await db.select().from(appSettings).where(eq(appSettings.id, "default")).limit(1);
|
|
const messages = {
|
|
registrationMessage: settings?.registrationMessage ?? DEFAULT_ADMISSION_MESSAGES.registrationMessage,
|
|
groupAccessDeniedMessage: settings?.groupAccessDeniedMessage ?? DEFAULT_ADMISSION_MESSAGES.groupAccessDeniedMessage,
|
|
vpnDeniedMessage: settings?.vpnDeniedMessage ?? DEFAULT_ADMISSION_MESSAGES.vpnDeniedMessage,
|
|
scheduledAccessDeniedMessage: settings?.scheduledAccessDeniedMessage ?? DEFAULT_ADMISSION_MESSAGES.scheduledAccessDeniedMessage,
|
|
};
|
|
const guildId = process.env.DISCORD_GUILD_ID?.trim();
|
|
const inviteUrl = process.env.DISCORD_INVITE_URL?.trim();
|
|
|
|
return (
|
|
<main className="mx-auto max-w-6xl px-6 py-14">
|
|
<div className="grid gap-10 lg:grid-cols-[0.7fr_1.3fr]">
|
|
<section>
|
|
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">System settings</p>
|
|
<h1 className="mt-4 font-display text-5xl font-black uppercase leading-none tracking-tight">Server gate</h1>
|
|
<p className="mt-6 max-w-sm leading-7 text-muted">The Discord guild and invite are deployment environment settings. Operators can adjust the message shown to denied Minecraft players here.</p>
|
|
<dl className="mt-7 space-y-4 font-mono text-[10px] uppercase tracking-wider text-muted">
|
|
<div><dt className="font-bold text-ink">Guild ID</dt><dd className="mt-1 break-all normal-case">{guildId ?? "Missing"}</dd></div>
|
|
<div><dt className="font-bold text-ink">Invite URL</dt><dd className="mt-1 break-all normal-case">{inviteUrl ? <a className="text-ink underline decoration-accent underline-offset-4" href={inviteUrl} rel="noreferrer" target="_blank">{inviteUrl}<span className="sr-only"> (opens in a new tab)</span></a> : "Missing"}</dd></div>
|
|
</dl>
|
|
</section>
|
|
|
|
<form action={saveAdmissionSettings} className="border border-line bg-panel p-7 shadow-[8px_8px_0_var(--color-shadow)] sm:p-9">
|
|
{query.saved && <p className="mb-6 border-l-2 border-signal pl-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">Settings saved</p>}
|
|
{query.error && <p className="mb-6 border-l-2 border-accent pl-4 text-sm" role="alert">Check the configuration value and try again.</p>}
|
|
|
|
<fieldset className="space-y-7">
|
|
<legend className="font-display text-2xl font-black uppercase">Minecraft denial messages</legend>
|
|
<p className="text-sm leading-6 text-muted">Each plain-text template is returned for one admission outcome. Messages must be between 10 and 500 characters. Registration, group, and network templates support <code>{"{player}"}</code> and <code>{"{group}"}</code>.</p>
|
|
<AdmissionMessageField description="Shown when the Minecraft identity is not registered. The unresolved group is everyone." label="Registration required" name="registrationMessage" value={messages.registrationMessage} />
|
|
<AdmissionMessageField description="Shown when the effective group has Minecraft access disabled." label="Group access disabled" name="groupAccessDeniedMessage" value={messages.groupAccessDeniedMessage} />
|
|
<AdmissionMessageField description="Shown outside a scheduled access window. Also supports {next_start} and {next_end}; generated times explicitly use UTC." label="Scheduled access denied" name="scheduledAccessDeniedMessage" value={messages.scheduledAccessDeniedMessage} />
|
|
<AdmissionMessageField description="Shown for VPN, proxy, or Tor connections when the effective group has no exception." label="VPN, proxy, or Tor denied" name="vpnDeniedMessage" value={messages.vpnDeniedMessage} />
|
|
</fieldset>
|
|
<button className="mt-8 border border-ink bg-ink px-6 py-3 font-mono text-xs font-bold uppercase tracking-[0.16em] text-canvas hover:bg-accent" type="submit">Save configuration</button>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function AdmissionMessageField({
|
|
description,
|
|
label,
|
|
name,
|
|
value,
|
|
}: {
|
|
description: string;
|
|
label: string;
|
|
name: "registrationMessage" | "groupAccessDeniedMessage" | "scheduledAccessDeniedMessage" | "vpnDeniedMessage";
|
|
value: string;
|
|
}) {
|
|
const descriptionId = `${name}-description`;
|
|
return (
|
|
<div>
|
|
<label className="block font-mono text-xs font-bold uppercase tracking-wider" htmlFor={name}>{label}</label>
|
|
<p className="mt-2 text-xs leading-5 text-muted" id={descriptionId}>{description}</p>
|
|
<textarea
|
|
aria-describedby={descriptionId}
|
|
className="mt-3 min-h-28 w-full resize-y border border-line bg-canvas px-4 py-3 text-sm leading-6 outline-none focus:border-accent"
|
|
defaultValue={value}
|
|
id={name}
|
|
maxLength={500}
|
|
minLength={10}
|
|
name={name}
|
|
required
|
|
/>
|
|
</div>
|
|
);
|
|
}
|