feat(admission): add group VPN exceptions
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { minecraftAccounts, users } from "@minecraft-account-manager/database";
|
||||
import { and, eq, ilike, isNull, or, sql } from "drizzle-orm";
|
||||
import { groups, minecraftAccounts, userGroupMemberships, users } from "@minecraft-account-manager/database";
|
||||
import { and, asc, desc, eq, ilike, isNull, or, sql } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { UserGroupSelect } from "@/components/user-group-select";
|
||||
import { db } from "@/lib/database";
|
||||
import { assignUserGroupFromRegistry } from "./actions";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminUsersPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ q?: string; error?: string }>;
|
||||
searchParams: Promise<{ q?: string; error?: string; saved?: string }>;
|
||||
}) {
|
||||
const query = await searchParams;
|
||||
const search = query.q?.trim().slice(0, 100) ?? "";
|
||||
@@ -31,33 +33,41 @@ export default async function AdminUsersPage({
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
firstName: users.firstName,
|
||||
discordUsername: users.discordUsername,
|
||||
discordGlobalName: users.discordGlobalName,
|
||||
discordUserId: users.discordUserId,
|
||||
onboardingCompletedAt: users.onboardingCompletedAt,
|
||||
primaryUsername: minecraftAccounts.username,
|
||||
accountCount: sql<number>`(
|
||||
select count(*)::int from ${minecraftAccounts} account_count
|
||||
where account_count.user_id = ${users.id}
|
||||
and account_count.deleted_at is null
|
||||
)`,
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(
|
||||
minecraftAccounts,
|
||||
and(
|
||||
eq(minecraftAccounts.userId, users.id),
|
||||
eq(minecraftAccounts.isPrimary, true),
|
||||
isNull(minecraftAccounts.deletedAt),
|
||||
),
|
||||
)
|
||||
.where(where)
|
||||
.orderBy(users.firstName, users.discordUsername)
|
||||
.limit(100);
|
||||
const [results, allGroups, memberships] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: users.id,
|
||||
firstName: users.firstName,
|
||||
discordUsername: users.discordUsername,
|
||||
discordGlobalName: users.discordGlobalName,
|
||||
discordUserId: users.discordUserId,
|
||||
onboardingCompletedAt: users.onboardingCompletedAt,
|
||||
primaryUsername: minecraftAccounts.username,
|
||||
accountCount: sql<number>`(
|
||||
select count(*)::int from ${minecraftAccounts} account_count
|
||||
where account_count.user_id = ${users.id}
|
||||
and account_count.deleted_at is null
|
||||
)`,
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(
|
||||
minecraftAccounts,
|
||||
and(
|
||||
eq(minecraftAccounts.userId, users.id),
|
||||
eq(minecraftAccounts.isPrimary, true),
|
||||
isNull(minecraftAccounts.deletedAt),
|
||||
),
|
||||
)
|
||||
.where(where)
|
||||
.orderBy(users.firstName, users.discordUsername)
|
||||
.limit(100),
|
||||
db.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault })
|
||||
.from(groups).orderBy(desc(groups.isDefault), asc(groups.name)),
|
||||
db.select({ userId: userGroupMemberships.userId, groupId: userGroupMemberships.groupId })
|
||||
.from(userGroupMemberships),
|
||||
]);
|
||||
const defaultGroup = allGroups.find((group) => group.isDefault);
|
||||
const groupByUser = new Map(memberships.map((membership) => [membership.userId, membership.groupId]));
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl px-6 py-14">
|
||||
@@ -79,13 +89,14 @@ export default async function AdminUsersPage({
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">The requested user could not be found.</p>}
|
||||
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">{query.error === "invalid-group-assignment" ? "The user or group no longer exists. No group change was applied." : "The requested user could not be found."}</p>}
|
||||
{query.saved === "group" && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 text-sm" role="status">User group updated.</p>}
|
||||
|
||||
<div className="mt-8 overflow-x-auto border border-line bg-panel shadow-[8px_8px_0_var(--color-shadow)]">
|
||||
<table className="w-full min-w-[760px] border-collapse text-left">
|
||||
<table className="w-full min-w-[900px] border-collapse text-left">
|
||||
<caption className="sr-only">Registered portal users</caption>
|
||||
<thead className="border-b border-line font-mono text-[10px] uppercase tracking-widest text-muted">
|
||||
<tr><th className="p-4" scope="col">User</th><th className="p-4" scope="col">Discord</th><th className="p-4" scope="col">Primary</th><th className="p-4" scope="col">Accounts</th><th className="p-4" scope="col">Status</th></tr>
|
||||
<tr><th className="p-4" scope="col">User</th><th className="p-4" scope="col">Discord</th><th className="p-4" scope="col">Primary</th><th className="p-4" scope="col">Accounts</th><th className="p-4" scope="col">Group</th><th className="p-4" scope="col">Status</th></tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-line">
|
||||
{results.map((user) => (
|
||||
@@ -94,10 +105,11 @@ export default async function AdminUsersPage({
|
||||
<td className="p-4"><div className="font-mono text-xs font-bold">{user.discordGlobalName ?? user.discordUsername}</div><div className="mt-1 font-mono text-[10px] text-muted">@{user.discordUsername}</div><div className="mt-1 font-mono text-[9px] text-muted">{user.discordUserId}</div></td>
|
||||
<td className="p-4 font-mono text-xs">{user.primaryUsername ?? "—"}</td>
|
||||
<td className="p-4 font-mono text-xs">{user.accountCount}</td>
|
||||
<td className="p-4">{defaultGroup ? <UserGroupSelect action={assignUserGroupFromRegistry} effectiveGroupId={groupByUser.get(user.id) ?? defaultGroup.id} groups={allGroups} search={search} userId={user.id} userLabel={user.firstName ?? user.discordUsername} /> : <span className="text-xs text-accent">Default group missing</span>}</td>
|
||||
<td className="p-4"><span className={`border px-2 py-1 font-mono text-[9px] uppercase tracking-wider ${user.onboardingCompletedAt ? "border-line text-muted" : "border-accent text-accent"}`}>{user.onboardingCompletedAt ? "Ready" : "Onboarding"}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
{!results.length && <tr><td className="p-8 text-muted" colSpan={5}>No users match that search.</td></tr>}
|
||||
{!results.length && <tr><td className="p-8 text-muted" colSpan={6}>No users match that search.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user