2023-04-20 04:20:00 -06:00
|
|
|
import { type NextPage } from "next";
|
|
|
|
import Head from "next/head";
|
|
|
|
import { type GetServerSideProps } from "next";
|
|
|
|
|
|
|
|
import { getServerAuthSession } from "../../server/auth";
|
|
|
|
import { api } from "~/utils/api";
|
|
|
|
import { IoTrashBinOutline } from "react-icons/io5";
|
|
|
|
import { AiOutlineClear } from "react-icons/ai";
|
|
|
|
import { FaShieldAlt } from "react-icons/fa";
|
2023-06-07 22:21:02 -06:00
|
|
|
import { SiGoogle, SiGithub } from "react-icons/si";
|
2023-04-20 04:20:00 -06:00
|
|
|
import type { Role } from "~/utils/types";
|
|
|
|
|
|
|
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
|
|
const session = await getServerAuthSession(ctx);
|
|
|
|
|
2023-05-31 16:49:11 -06:00
|
|
|
// Redirect to login if not signed in
|
2023-04-20 04:20:00 -06:00
|
|
|
if (!session) {
|
|
|
|
return {
|
|
|
|
redirect: {
|
2023-05-31 16:49:11 -06:00
|
|
|
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
2023-04-20 04:20:00 -06:00
|
|
|
permanent: false,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (session.user.role !== "ADMIN") {
|
|
|
|
ctx.res.statusCode = 403;
|
|
|
|
return {
|
|
|
|
redirect: {
|
|
|
|
destination: "/",
|
|
|
|
permanent: false,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
// Return session if logged in
|
|
|
|
return {
|
|
|
|
props: { session },
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
const Admin: NextPage = () => {
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Head>
|
|
|
|
<title>Sprint Padawan - Admin</title>
|
|
|
|
<meta name="description" content="Plan. Sprint. Repeat." />
|
|
|
|
</Head>
|
|
|
|
<div className="flex flex-col items-center justify-center text-center px-4 py-16 ">
|
2023-06-05 16:56:28 -06:00
|
|
|
<div className="flex flex-col items-center">
|
2023-04-20 04:20:00 -06:00
|
|
|
<AdminBody />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Admin;
|
|
|
|
|
|
|
|
const AdminBody: React.FC = () => {
|
|
|
|
const {
|
|
|
|
data: usersCount,
|
|
|
|
isLoading: usersCountLoading,
|
|
|
|
isFetching: usersCountFetching,
|
|
|
|
refetch: refetchUsersCount,
|
|
|
|
} = api.user.countAll.useQuery();
|
|
|
|
const {
|
|
|
|
data: users,
|
|
|
|
isLoading: usersLoading,
|
|
|
|
isFetching: usersFetching,
|
|
|
|
refetch: refetchUsers,
|
|
|
|
} = api.user.getAll.useQuery();
|
|
|
|
const {
|
|
|
|
data: roomsCount,
|
|
|
|
isLoading: roomsCountLoading,
|
|
|
|
isFetching: roomsCountFetching,
|
|
|
|
refetch: refetchRoomsCount,
|
|
|
|
} = api.room.countAll.useQuery();
|
|
|
|
const {
|
|
|
|
data: votesCount,
|
|
|
|
isLoading: votesCountLoading,
|
|
|
|
isFetching: votesCountFetching,
|
|
|
|
refetch: refetchVotesCount,
|
|
|
|
} = api.vote.countAll.useQuery();
|
|
|
|
|
2023-06-07 22:21:02 -06:00
|
|
|
const getProviders = (user: {
|
|
|
|
createdAt: Date;
|
|
|
|
accounts: {
|
|
|
|
provider: string;
|
|
|
|
}[];
|
|
|
|
sessions: {
|
|
|
|
id: string;
|
|
|
|
}[];
|
|
|
|
id: string;
|
|
|
|
role: Role;
|
|
|
|
name: string | null;
|
|
|
|
email: string | null;
|
|
|
|
}) => {
|
|
|
|
return user.accounts.map((account) => {
|
|
|
|
return account.provider;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-04-20 04:20:00 -06:00
|
|
|
const deleteUserMutation = api.user.delete.useMutation({
|
|
|
|
onSuccess: async () => {
|
|
|
|
await refetchData();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const clearSessionsMutation = api.session.deleteAll.useMutation({
|
|
|
|
onSuccess: async () => {
|
|
|
|
await refetchData();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const setRoleMutation = api.user.setRole.useMutation({
|
|
|
|
onSuccess: async () => {
|
|
|
|
await refetchData();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
const deleteUserHandler = async (userId: string) => {
|
|
|
|
await deleteUserMutation.mutateAsync({ userId });
|
|
|
|
};
|
|
|
|
|
|
|
|
const clearSessionsHandler = async (userId: string) => {
|
|
|
|
await clearSessionsMutation.mutateAsync({ userId });
|
|
|
|
};
|
|
|
|
|
|
|
|
const setUserRoleHandler = async (userId: string, role: Role) => {
|
|
|
|
await setRoleMutation.mutateAsync({ userId, role });
|
|
|
|
};
|
|
|
|
|
|
|
|
const refetchData = async () => {
|
|
|
|
await Promise.all([
|
|
|
|
refetchUsers(),
|
|
|
|
refetchUsersCount(),
|
|
|
|
refetchRoomsCount(),
|
|
|
|
refetchVotesCount(),
|
|
|
|
]);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
2023-06-05 16:56:28 -06:00
|
|
|
<h1 className="text-4xl font-bold">Admin Panel</h1>
|
2023-04-20 04:20:00 -06:00
|
|
|
|
|
|
|
<div className="stats stats-horizontal shadow bg-neutral m-4">
|
|
|
|
<div className="stat">
|
|
|
|
<div className="stat-title">Users</div>
|
|
|
|
<div className="stat-value">
|
|
|
|
{usersCountLoading || usersCountFetching ? (
|
2023-06-19 23:46:12 -06:00
|
|
|
<span className="loading loading-dots loading-lg"></span>
|
2023-04-20 04:20:00 -06:00
|
|
|
) : (
|
|
|
|
<>{usersCount ? usersCount : "0"}</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="stat">
|
|
|
|
<div className="stat-title">Rooms</div>
|
|
|
|
<div className="stat-value">
|
|
|
|
{roomsCountLoading || roomsCountFetching ? (
|
2023-06-19 23:46:12 -06:00
|
|
|
<span className="loading loading-dots loading-lg"></span>
|
2023-04-20 04:20:00 -06:00
|
|
|
) : (
|
|
|
|
<>{roomsCount ? roomsCount : "0"}</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="stat">
|
|
|
|
<div className="stat-title">Votes</div>
|
|
|
|
<div className="stat-value">
|
|
|
|
{votesCountLoading || votesCountFetching ? (
|
2023-06-19 23:46:12 -06:00
|
|
|
<span className="loading loading-dots loading-lg"></span>
|
2023-04-20 04:20:00 -06:00
|
|
|
) : (
|
|
|
|
<>{votesCount ? votesCount : "0"}</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{usersCountFetching ||
|
|
|
|
usersFetching ||
|
|
|
|
roomsCountFetching ||
|
|
|
|
votesCountFetching ? (
|
|
|
|
<button className="btn btn-primary loading">Fetching...</button>
|
|
|
|
) : (
|
|
|
|
<button className="btn btn-primary" onClick={() => void refetchData()}>
|
|
|
|
Re-fetch
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
|
|
|
|
<div className="card max-w-[80vw] bg-neutral shadow-xl m-4">
|
|
|
|
<div className="card-body">
|
|
|
|
<h2 className="card-title">Users:</h2>
|
|
|
|
|
|
|
|
{usersLoading || usersFetching ? (
|
2023-06-19 23:46:12 -06:00
|
|
|
<span className="loading loading-dots loading-lg"></span>
|
2023-04-20 04:20:00 -06:00
|
|
|
) : (
|
2023-06-05 12:37:10 -06:00
|
|
|
<div className="overflow-x-scroll">
|
|
|
|
<table className="table text-center">
|
2023-04-20 04:20:00 -06:00
|
|
|
{/* head */}
|
|
|
|
<thead>
|
2023-06-05 12:37:10 -06:00
|
|
|
<tr className="border-white">
|
2023-04-20 04:20:00 -06:00
|
|
|
<th>ID</th>
|
|
|
|
<th>Name</th>
|
|
|
|
<th>Created At</th>
|
|
|
|
<th># Sessions</th>
|
2023-06-07 22:21:02 -06:00
|
|
|
<th>Providers</th>
|
2023-04-20 04:20:00 -06:00
|
|
|
<th>Actions</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody className="">
|
|
|
|
{users
|
|
|
|
?.sort((user1, user2) =>
|
|
|
|
user2.createdAt > user1.createdAt ? 1 : -1
|
|
|
|
)
|
|
|
|
.map((user) => {
|
|
|
|
return (
|
2023-06-05 12:37:10 -06:00
|
|
|
<tr key={user.id} className="hover">
|
2023-04-20 04:20:00 -06:00
|
|
|
<td className="max-w-[100px] break-words">
|
|
|
|
{user.id}
|
|
|
|
</td>
|
|
|
|
|
|
|
|
<td className="max-w-[100px] break-normal">
|
|
|
|
{user.name}
|
|
|
|
</td>
|
|
|
|
<td className="max-w-[100px] break-normal">
|
|
|
|
{user.createdAt.toLocaleDateString()}
|
|
|
|
</td>
|
|
|
|
<td className="max-w-[100px] break-normal">
|
|
|
|
{user.sessions.length}
|
|
|
|
</td>
|
2023-06-07 22:21:02 -06:00
|
|
|
<td className="max-w-[100px] break-normal">
|
|
|
|
{getProviders(user).includes("google") && (
|
|
|
|
<SiGoogle className="text-xl m-1 inline-block hover:text-secondary" />
|
|
|
|
)}
|
|
|
|
{getProviders(user).includes("github") && (
|
|
|
|
<SiGithub className="text-xl m-1 inline-block hover:text-secondary" />
|
|
|
|
)}
|
|
|
|
</td>
|
2023-04-20 04:20:00 -06:00
|
|
|
<td>
|
|
|
|
<button className="m-2">
|
|
|
|
{user.role === "ADMIN" ? (
|
|
|
|
<FaShieldAlt
|
|
|
|
className="text-xl inline-block text-primary"
|
|
|
|
onClick={() =>
|
|
|
|
void setUserRoleHandler(user.id, "USER")
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<FaShieldAlt
|
|
|
|
className="text-xl inline-block"
|
|
|
|
onClick={() =>
|
|
|
|
void setUserRoleHandler(user.id, "ADMIN")
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
className="m-2"
|
|
|
|
onClick={() => void clearSessionsHandler(user.id)}
|
|
|
|
>
|
|
|
|
<AiOutlineClear className="text-xl inline-block hover:text-warning" />
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
className="m-2"
|
|
|
|
onClick={() => void deleteUserHandler(user.id)}
|
|
|
|
>
|
|
|
|
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
|
|
|
</button>
|
|
|
|
</td>
|
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|