Add player controls and configurable profiles
This commit is contained in:
@@ -10,12 +10,14 @@ import { getShellContext } from "@/lib/shell";
|
||||
import { requireCurrentUser, userIsAdmin } from "@/lib/session";
|
||||
import { requireInitialSetup } from "@/lib/setup";
|
||||
import { DataTable, EmptyState, MetricTile, PageHeader, Panel, StatusDot, Tabs } from "@/components/ui";
|
||||
import { getAppSettings, type AppSettings } from "@/lib/settings";
|
||||
import { updateInstanceSettings, updateSecuritySettings } from "@/lib/settings-actions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const adminTabs = ["Users", "Rooms", "Roles", "Invites", "Instance", "Security"];
|
||||
|
||||
export default async function AdminPage({ searchParams }: { searchParams: Promise<{ tab?: string; q?: string }> }) {
|
||||
export default async function AdminPage({ searchParams }: { searchParams: Promise<{ tab?: string; q?: string; saved?: string }> }) {
|
||||
await requireInitialSetup();
|
||||
const user = await requireCurrentUser();
|
||||
|
||||
@@ -23,12 +25,12 @@ export default async function AdminPage({ searchParams }: { searchParams: Promis
|
||||
redirect("/dashboard");
|
||||
}
|
||||
|
||||
const { tab = "Users", q = "" } = await searchParams;
|
||||
const { tab = "Users", q = "", saved } = await searchParams;
|
||||
const activeTab = adminTabs.includes(tab) ? tab : "Users";
|
||||
const query = q.trim().toLowerCase();
|
||||
const shell = await getShellContext(user);
|
||||
|
||||
const [users, rooms, roles, pendingRequests] = await Promise.all([
|
||||
const [users, rooms, roles, pendingRequests, appSettings] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
include: { roles: { include: { role: true } }, _count: { select: { ownedRooms: true, roomMembers: true } } },
|
||||
orderBy: { createdAt: "asc" }
|
||||
@@ -41,7 +43,8 @@ export default async function AdminPage({ searchParams }: { searchParams: Promis
|
||||
include: { _count: { select: { users: true, permissions: true } } },
|
||||
orderBy: { name: "asc" }
|
||||
}),
|
||||
prisma.friendship.count({ where: { status: "PENDING" } })
|
||||
prisma.friendship.count({ where: { status: "PENDING" } }),
|
||||
getAppSettings()
|
||||
]);
|
||||
const filteredUsers = query
|
||||
? users.filter((account) =>
|
||||
@@ -103,8 +106,8 @@ export default async function AdminPage({ searchParams }: { searchParams: Promis
|
||||
{activeTab === "Rooms" ? <RoomsTable rooms={filteredRooms} /> : null}
|
||||
{activeTab === "Roles" ? <RolesTable roles={filteredRoles} /> : null}
|
||||
{activeTab === "Invites" ? <InvitesPanel pendingRequests={pendingRequests} /> : null}
|
||||
{activeTab === "Instance" ? <InstancePanel userCount={users.length} roomCount={rooms.length} /> : null}
|
||||
{activeTab === "Security" ? <SecurityPanel /> : null}
|
||||
{activeTab === "Instance" ? <InstancePanel userCount={users.length} roomCount={rooms.length} settings={appSettings} saved={saved} /> : null}
|
||||
{activeTab === "Security" ? <SecurityPanel settings={appSettings} saved={saved} /> : null}
|
||||
</Panel>
|
||||
</AppShell>
|
||||
);
|
||||
@@ -117,6 +120,7 @@ function UsersTable({
|
||||
id: string;
|
||||
username: string;
|
||||
displayName: string | null;
|
||||
avatarUrl: string | null;
|
||||
createdAt: Date;
|
||||
roles: Array<{ roleId: string; role: { name: string } }>;
|
||||
_count: { ownedRooms: number; roomMembers: number };
|
||||
@@ -139,7 +143,7 @@ function UsersTable({
|
||||
<tr key={account.id}>
|
||||
<td>
|
||||
<div className="row-main">
|
||||
<Avatar name={account.displayName || account.username} size={30} />
|
||||
<Avatar name={account.displayName || account.username} src={account.avatarUrl} size={30} />
|
||||
<div className="row-title">
|
||||
<strong>{account.displayName || account.username}</strong>
|
||||
<span>@{account.username}</span>
|
||||
@@ -275,9 +279,45 @@ function InvitesPanel({ pendingRequests }: { pendingRequests: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
function InstancePanel({ userCount, roomCount }: { userCount: number; roomCount: number }) {
|
||||
function InstancePanel({ userCount, roomCount, settings, saved }: { userCount: number; roomCount: number; settings: AppSettings; saved?: string }) {
|
||||
return (
|
||||
<div className="split-grid">
|
||||
<Panel title="Instance Settings" eyebrow="Site-wide">
|
||||
{saved ? <p className="form-success">Instance settings saved.</p> : null}
|
||||
<form className="form compact-form" action={updateInstanceSettings}>
|
||||
<label>
|
||||
Instance name
|
||||
<input className="input" name="instanceName" defaultValue={settings.instanceName} maxLength={80} />
|
||||
</label>
|
||||
<label>
|
||||
Description
|
||||
<textarea className="input textarea" name="instanceDescription" defaultValue={settings.instanceDescription} maxLength={240} />
|
||||
</label>
|
||||
<label>
|
||||
Default room visibility
|
||||
<select className="input" name="defaultRoomVisibility" defaultValue={settings.defaultRoomVisibility}>
|
||||
<option value="FRIENDS">Friends-only</option>
|
||||
<option value="PUBLIC">Public</option>
|
||||
<option value="EXPLICIT">Explicit members</option>
|
||||
<option value="ROLE_RESTRICTED">Role restricted</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Avatar upload limit in MB
|
||||
<input className="input" name="maxAvatarUploadMb" type="number" min="1" max="10" defaultValue={settings.maxAvatarUploadMb} />
|
||||
</label>
|
||||
<fieldset className="checkbox-grid">
|
||||
<legend>Allowed media providers</legend>
|
||||
{["YOUTUBE", "TWITCH", "DIRECT"].map((provider) => (
|
||||
<label key={provider}>
|
||||
<input name="allowedProviders" type="checkbox" value={provider} defaultChecked={settings.allowedProviders.includes(provider)} />
|
||||
{provider}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
<button className="button primary" type="submit">Save instance settings</button>
|
||||
</form>
|
||||
</Panel>
|
||||
<Panel title="Runtime" eyebrow="Container">
|
||||
<div className="settings-list">
|
||||
<Setting icon={<Database size={16} />} label="Database" value="Postgres via DATABASE_URL" />
|
||||
@@ -297,15 +337,27 @@ function InstancePanel({ userCount, roomCount }: { userCount: number; roomCount:
|
||||
);
|
||||
}
|
||||
|
||||
function SecurityPanel() {
|
||||
function SecurityPanel({ settings, saved }: { settings: AppSettings; saved?: string }) {
|
||||
return (
|
||||
<div className="split-grid">
|
||||
<Panel title="Security Controls" eyebrow="Policy">
|
||||
<div className="settings-list">
|
||||
<Setting icon={<LockKeyhole size={16} />} label="Password storage" value="bcrypt hash" />
|
||||
<Setting icon={<Shield size={16} />} label="Admin check" value="Server-side role lookup" />
|
||||
<Setting icon={<AlertTriangle size={16} />} label="Registration mode" value="Open registration" />
|
||||
</div>
|
||||
{saved ? <p className="form-success">Security settings saved.</p> : null}
|
||||
<form className="form compact-form" action={updateSecuritySettings}>
|
||||
<label>
|
||||
Registration mode
|
||||
<select className="input" name="registrationMode" defaultValue={settings.registrationMode}>
|
||||
<option value="OPEN">Open registration</option>
|
||||
<option value="INVITE_ONLY">Invite only</option>
|
||||
<option value="DISABLED">Disabled</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="settings-list">
|
||||
<Setting icon={<LockKeyhole size={16} />} label="Password storage" value="bcrypt hash" />
|
||||
<Setting icon={<Shield size={16} />} label="Admin check" value="Server-side role lookup" />
|
||||
<Setting icon={<AlertTriangle size={16} />} label="Registration mode" value={settings.registrationMode.toLowerCase().replace("_", " ")} />
|
||||
</div>
|
||||
<button className="button primary" type="submit">Save security settings</button>
|
||||
</form>
|
||||
</Panel>
|
||||
<Panel title="Danger Zone" eyebrow="Destructive">
|
||||
<EmptyState title="Destructive actions are disabled" description="User banning, registration mode changes, and deletion need server actions before controls become active." />
|
||||
|
||||
Reference in New Issue
Block a user