mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-24 13:56:39 +00:00
server admin enforce 2fa per user
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { UsersDataTable } from "./AdminUsersDataTable";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowRight, ArrowUpDown } from "lucide-react";
|
||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
@@ -12,6 +12,12 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
|
||||
export type GlobalUserRow = {
|
||||
id: string;
|
||||
@@ -22,6 +28,8 @@ export type GlobalUserRow = {
|
||||
idpId: number | null;
|
||||
idpName: string;
|
||||
dateCreated: string;
|
||||
twoFactorEnabled: boolean | null;
|
||||
twoFactorSetupRequested: boolean | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -41,11 +49,11 @@ export default function UsersTable({ users }: Props) {
|
||||
const deleteUser = (id: string) => {
|
||||
api.delete(`/user/${id}`)
|
||||
.catch((e) => {
|
||||
console.error(t('userErrorDelete'), e);
|
||||
console.error(t("userErrorDelete"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('userErrorDelete'),
|
||||
description: formatAxiosError(e, t('userErrorDelete'))
|
||||
title: t("userErrorDelete"),
|
||||
description: formatAxiosError(e, t("userErrorDelete"))
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
@@ -84,7 +92,7 @@ export default function UsersTable({ users }: Props) {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t('username')}
|
||||
{t("username")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -100,7 +108,7 @@ export default function UsersTable({ users }: Props) {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t('email')}
|
||||
{t("email")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -116,7 +124,7 @@ export default function UsersTable({ users }: Props) {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -132,28 +140,85 @@ export default function UsersTable({ users }: Props) {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t('identityProvider')}
|
||||
{t("identityProvider")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "twoFactorEnabled",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("twoFactor")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span>
|
||||
{userRow.twoFactorEnabled ||
|
||||
userRow.twoFactorSetupRequested ? (
|
||||
<span className="text-green-500">
|
||||
{t("enabled")}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t("disabled")}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelected(r);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant={"outlinePrimary"}
|
||||
className="ml-2"
|
||||
variant={"secondary"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelected(r);
|
||||
setIsDeleteModalOpen(true);
|
||||
router.push(`/admin/users/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{t('delete')}
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -174,26 +239,27 @@ export default function UsersTable({ users }: Props) {
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t('userQuestionRemove', {selectedUser: selected?.email || selected?.name || selected?.username})}
|
||||
{t("userQuestionRemove", {
|
||||
selectedUser:
|
||||
selected?.email ||
|
||||
selected?.name ||
|
||||
selected?.username
|
||||
})}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>
|
||||
{t('userMessageRemove')}
|
||||
</b>
|
||||
<b>{t("userMessageRemove")}</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{t('userMessageConfirm')}
|
||||
</p>
|
||||
<p>{t("userMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t('userDeleteConfirm')}
|
||||
buttonText={t("userDeleteConfirm")}
|
||||
onConfirm={async () => deleteUser(selected!.id)}
|
||||
string={
|
||||
selected.email || selected.name || selected.username
|
||||
}
|
||||
title={t('userDeleteServer')}
|
||||
title={t("userDeleteServer")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user