commit
8fb8f5140f
9 changed files with 182 additions and 127 deletions
|
@ -1,8 +1,3 @@
|
||||||
enum RoleValue {
|
|
||||||
USER
|
|
||||||
ADMIN
|
|
||||||
}
|
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
@ -46,7 +41,6 @@ model Session {
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
role RoleValue @default(USER)
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
name String?
|
name String?
|
||||||
email String? @unique
|
email String? @unique
|
||||||
|
@ -57,6 +51,9 @@ model User {
|
||||||
rooms Room[]
|
rooms Room[]
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
logs Log[]
|
logs Log[]
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
isVIP Boolean @default(false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
|
|
|
@ -85,7 +85,7 @@ const Navbar: React.FC<NavbarProps> = ({ title }) => {
|
||||||
Profile
|
Profile
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{sessionData.user.role === "ADMIN" && (
|
{sessionData.user.isAdmin && (
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
about="Admin Page"
|
about="Admin Page"
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { AiOutlineClear } from "react-icons/ai";
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
import { FaShieldAlt } from "react-icons/fa";
|
||||||
import { IoTrashBinOutline } from "react-icons/io5";
|
import { IoTrashBinOutline } from "react-icons/io5";
|
||||||
import { SiGithub, SiGoogle } from "react-icons/si";
|
import { SiGithub, SiGoogle } from "react-icons/si";
|
||||||
|
import { GiStarFormation } from "react-icons/gi";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import type { Role } from "~/utils/types";
|
|
||||||
import { getServerAuthSession } from "../../server/auth";
|
import { getServerAuthSession } from "../../server/auth";
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
|
@ -22,7 +22,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.user.role !== "ADMIN") {
|
if (!session.user.isAdmin) {
|
||||||
ctx.res.statusCode = 403;
|
ctx.res.statusCode = 403;
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
|
@ -90,7 +90,8 @@ const AdminBody: React.FC = () => {
|
||||||
id: string;
|
id: string;
|
||||||
}[];
|
}[];
|
||||||
id: string;
|
id: string;
|
||||||
role: Role;
|
isAdmin: boolean;
|
||||||
|
isVIP: boolean;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -119,7 +120,13 @@ const AdminBody: React.FC = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRoleMutation = api.user.setRole.useMutation({
|
const setAdminMutation = api.user.setAdmin.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
await refetchData();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setVIPMutation = api.user.setVIP.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await refetchData();
|
await refetchData();
|
||||||
},
|
},
|
||||||
|
@ -137,8 +144,12 @@ const AdminBody: React.FC = () => {
|
||||||
await clearSessionsMutation.mutateAsync();
|
await clearSessionsMutation.mutateAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUserRoleHandler = async (userId: string, role: Role) => {
|
const setAdmin = async (userId: string, value: boolean) => {
|
||||||
await setRoleMutation.mutateAsync({ userId, role });
|
await setAdminMutation.mutateAsync({ userId, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setVIP = async (userId: string, value: boolean) => {
|
||||||
|
await setVIPMutation.mutateAsync({ userId, value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const refetchData = async () => {
|
const refetchData = async () => {
|
||||||
|
@ -158,70 +169,70 @@ const AdminBody: React.FC = () => {
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title">Users</div>
|
<div className="stat-title">Users</div>
|
||||||
<div className="stat-value">
|
<div className="stat-value">
|
||||||
{ usersCountLoading || usersCountFetching ? (
|
{usersCountLoading || usersCountFetching ? (
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
) : (
|
) : (
|
||||||
<>{ usersCount ? usersCount : "0" }</>
|
<>{usersCount ? usersCount : "0"}</>
|
||||||
) }
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title">Rooms</div>
|
<div className="stat-title">Rooms</div>
|
||||||
<div className="stat-value">
|
<div className="stat-value">
|
||||||
{ roomsCountLoading || roomsCountFetching ? (
|
{roomsCountLoading || roomsCountFetching ? (
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
) : (
|
) : (
|
||||||
<>{ roomsCount ? roomsCount : "0" }</>
|
<>{roomsCount ? roomsCount : "0"}</>
|
||||||
) }
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat">
|
<div className="stat">
|
||||||
<div className="stat-title">Votes</div>
|
<div className="stat-title">Votes</div>
|
||||||
<div className="stat-value">
|
<div className="stat-value">
|
||||||
{ votesCountLoading || votesCountFetching ? (
|
{votesCountLoading || votesCountFetching ? (
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
) : (
|
) : (
|
||||||
<>{ votesCount ? votesCount : "0" }</>
|
<>{votesCount ? votesCount : "0"}</>
|
||||||
) }
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ usersCountFetching ||
|
{usersCountFetching ||
|
||||||
usersFetching ||
|
usersFetching ||
|
||||||
roomsCountFetching ||
|
roomsCountFetching ||
|
||||||
votesCountFetching ? (
|
votesCountFetching ? (
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary m-2"
|
className="btn btn-primary m-2"
|
||||||
onClick={ () => void clearSessionsHandler() }
|
onClick={() => void clearSessionsHandler()}
|
||||||
>
|
>
|
||||||
Delete All Sessions
|
Delete All Sessions
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={ () => void refetchData() }
|
onClick={() => void refetchData()}
|
||||||
>
|
>
|
||||||
Re-fetch
|
Re-fetch
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) }
|
)}
|
||||||
|
|
||||||
<div className="card max-w-[80vw] bg-neutral shadow-xl m-4">
|
<div className="card max-w-[80vw] bg-neutral shadow-xl m-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">Users:</h2>
|
<h2 className="card-title">Users:</h2>
|
||||||
|
|
||||||
{ usersLoading || usersFetching ? (
|
{usersLoading || usersFetching ? (
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-scroll">
|
<div className="overflow-x-scroll">
|
||||||
<table className="table text-center">
|
<table className="table text-center">
|
||||||
{/* head */ }
|
{/* head */}
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-white">
|
<tr className="border-white">
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
@ -233,55 +244,64 @@ const AdminBody: React.FC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="">
|
<tbody className="">
|
||||||
{ users
|
{users
|
||||||
?.sort((user1, user2) =>
|
?.sort((user1, user2) =>
|
||||||
user2.createdAt > user1.createdAt ? 1 : -1
|
user2.createdAt > user1.createdAt ? 1 : -1
|
||||||
)
|
)
|
||||||
.map((user) => {
|
.map((user) => {
|
||||||
return (
|
return (
|
||||||
<tr key={ user.id } className="hover">
|
<tr key={user.id} className="hover">
|
||||||
<td className="max-w-[100px] break-words">
|
<td className="max-w-[100px] break-words">
|
||||||
{ user.id }
|
{user.id}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="max-w-[100px] break-normal">
|
<td className="max-w-[100px] break-normal">
|
||||||
{ user.name }
|
{user.name}
|
||||||
</td>
|
</td>
|
||||||
<td className="max-w-[100px] break-normal">
|
<td className="max-w-[100px] break-normal">
|
||||||
{ user.createdAt.toLocaleDateString() }
|
{user.createdAt.toLocaleDateString()}
|
||||||
</td>
|
</td>
|
||||||
<td className="max-w-[100px] break-normal">
|
<td className="max-w-[100px] break-normal">
|
||||||
{ user.sessions.length }
|
{user.sessions.length}
|
||||||
</td>
|
</td>
|
||||||
<td className="max-w-[100px] break-normal">
|
<td className="max-w-[100px] break-normal">
|
||||||
{ getProviders(user).includes("google") && (
|
{getProviders(user).includes("google") && (
|
||||||
<SiGoogle className="text-xl m-1 inline-block hover:text-secondary" />
|
<SiGoogle className="text-xl m-1 inline-block hover:text-secondary" />
|
||||||
) }
|
)}
|
||||||
{ getProviders(user).includes("github") && (
|
{getProviders(user).includes("github") && (
|
||||||
<SiGithub className="text-xl m-1 inline-block hover:text-secondary" />
|
<SiGithub className="text-xl m-1 inline-block hover:text-secondary" />
|
||||||
) }
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="m-2">
|
<button className="m-2">
|
||||||
{ user.role === "ADMIN" ? (
|
{user.isAdmin ? (
|
||||||
<FaShieldAlt
|
<FaShieldAlt
|
||||||
className="text-xl inline-block text-primary"
|
className="text-xl inline-block text-primary"
|
||||||
onClick={ () =>
|
onClick={() => void setAdmin(user.id, false)}
|
||||||
void setUserRoleHandler(user.id, "USER")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FaShieldAlt
|
<FaShieldAlt
|
||||||
className="text-xl inline-block"
|
className="text-xl inline-block"
|
||||||
onClick={ () =>
|
onClick={() => void setAdmin(user.id, true)}
|
||||||
void setUserRoleHandler(user.id, "ADMIN")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) }
|
)}
|
||||||
|
</button>
|
||||||
|
<button className="m-2">
|
||||||
|
{user.isVIP ? (
|
||||||
|
<GiStarFormation
|
||||||
|
className="text-xl inline-block text-secondary"
|
||||||
|
onClick={() => void setVIP(user.id, false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GiStarFormation
|
||||||
|
className="text-xl inline-block"
|
||||||
|
onClick={() => void setVIP(user.id, true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="m-2"
|
className="m-2"
|
||||||
onClick={ () =>
|
onClick={() =>
|
||||||
void clearSessionsByUserHandler(user.id)
|
void clearSessionsByUserHandler(user.id)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -289,18 +309,18 @@ const AdminBody: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="m-2"
|
className="m-2"
|
||||||
onClick={ () => void deleteUserHandler(user.id) }
|
onClick={() => void deleteUserHandler(user.id)}
|
||||||
>
|
>
|
||||||
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}) }
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) }
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
import { FaShieldAlt } from "react-icons/fa";
|
||||||
import { getServerAuthSession } from "~/server/auth";
|
import { getServerAuthSession } from "~/server/auth";
|
||||||
|
import { GiStarFormation } from "react-icons/gi";
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
const session = await getServerAuthSession(ctx);
|
const session = await getServerAuthSession(ctx);
|
||||||
|
@ -57,20 +58,23 @@ const HomePageBody: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto">
|
<h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto">
|
||||||
Hi, { sessionData?.user.name }!{ " " }
|
Hi, {sessionData?.user.name}!{" "}
|
||||||
{ sessionData?.user.role === "ADMIN" && (
|
{sessionData?.user.isAdmin && (
|
||||||
<FaShieldAlt className="inline-block text-primary" />
|
<FaShieldAlt className="inline-block text-primary" />
|
||||||
) }
|
)}
|
||||||
|
{sessionData?.user.isVIP && (
|
||||||
|
<GiStarFormation className="inline-block text-secondary" />
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="tabs tabs-boxed border-2 border-cyan-500 mb-4">
|
<div className="tabs tabs-boxed border-2 border-cyan-500 mb-4">
|
||||||
<a
|
<a
|
||||||
className={
|
className={
|
||||||
tabIndex === 0 ? "tab no-underline tab-active" : "tab no-underline"
|
tabIndex === 0 ? "tab no-underline tab-active" : "tab no-underline"
|
||||||
}
|
}
|
||||||
onClick={ () => {
|
onClick={() => {
|
||||||
setTabIndex(0);
|
setTabIndex(0);
|
||||||
localStorage.setItem("dashboardTabIndex", "0");
|
localStorage.setItem("dashboardTabIndex", "0");
|
||||||
} }
|
}}
|
||||||
>
|
>
|
||||||
Join a Room
|
Join a Room
|
||||||
</a>
|
</a>
|
||||||
|
@ -78,36 +82,36 @@ const HomePageBody: React.FC = () => {
|
||||||
className={
|
className={
|
||||||
tabIndex === 1 ? "tab no-underline tab-active" : "tab no-underline"
|
tabIndex === 1 ? "tab no-underline tab-active" : "tab no-underline"
|
||||||
}
|
}
|
||||||
onClick={ () => {
|
onClick={() => {
|
||||||
setTabIndex(1);
|
setTabIndex(1);
|
||||||
localStorage.setItem("dashboardTabIndex", "1");
|
localStorage.setItem("dashboardTabIndex", "1");
|
||||||
} }
|
}}
|
||||||
>
|
>
|
||||||
Room List
|
Room List
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ tabIndex === 0 && (
|
{tabIndex === 0 && (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter Room ID"
|
placeholder="Enter Room ID"
|
||||||
className="input input-bordered input-primary mb-4"
|
className="input input-bordered input-primary mb-4"
|
||||||
onChange={ (event) => {
|
onChange={(event) => {
|
||||||
console.log(event.target.value);
|
console.log(event.target.value);
|
||||||
setJoinRoomTextBox(event.target.value);
|
setJoinRoomTextBox(event.target.value);
|
||||||
} }
|
}}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
href={ joinRoomTextBox.length > 0 ? `/room/${joinRoomTextBox}` : "/" }
|
href={joinRoomTextBox.length > 0 ? `/room/${joinRoomTextBox}` : "/"}
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
>
|
>
|
||||||
Join Room
|
Join Room
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
) }
|
)}
|
||||||
|
|
||||||
{ tabIndex === 1 && <RoomList /> }
|
{tabIndex === 1 && <RoomList />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { FaShieldAlt } from "react-icons/fa";
|
||||||
import { SiGithub, SiGoogle } from "react-icons/si";
|
import { SiGithub, SiGoogle } from "react-icons/si";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { getServerAuthSession } from "../../server/auth";
|
import { getServerAuthSession } from "../../server/auth";
|
||||||
|
import { GiStarFormation } from "react-icons/gi";
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
const session = await getServerAuthSession(ctx);
|
const session = await getServerAuthSession(ctx);
|
||||||
|
@ -103,7 +104,7 @@ const ProfileBody: React.FC = () => {
|
||||||
<label
|
<label
|
||||||
htmlFor="delete-user-modal"
|
htmlFor="delete-user-modal"
|
||||||
className="btn btn-error"
|
className="btn btn-error"
|
||||||
onClick={ () => void deleteCurrentUser() }
|
onClick={() => void deleteCurrentUser()}
|
||||||
>
|
>
|
||||||
I am sure!
|
I am sure!
|
||||||
</label>
|
</label>
|
||||||
|
@ -114,79 +115,82 @@ const ProfileBody: React.FC = () => {
|
||||||
<div className="card w-90 bg-neutral shadow-xl">
|
<div className="card w-90 bg-neutral shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">Profile:</h2>
|
<h2 className="card-title">Profile:</h2>
|
||||||
{ sessionData.user.image && (
|
{sessionData.user.image && (
|
||||||
<div className="indicator mx-auto m-4">
|
<Image
|
||||||
{ sessionData.user.role === "ADMIN" && (
|
className="mx-auto"
|
||||||
<span className="indicator-item indicator-bottom badge badge-primary">
|
src={sessionData.user.image}
|
||||||
<div className="tooltip tooltip-primary" data-tip="Admin">
|
alt="Profile picture."
|
||||||
<FaShieldAlt className="text-xl" />
|
height={100}
|
||||||
</div>
|
width={100}
|
||||||
</span>
|
priority
|
||||||
) }
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Image
|
<div className="flex flex-row flex-wrap items-center text-center justify-center gap-4">
|
||||||
className="mx-auto"
|
{sessionData.user.isAdmin && (
|
||||||
src={ sessionData.user.image }
|
<div className="tooltip tooltip-primary" data-tip="Admin">
|
||||||
alt="Profile picture."
|
<FaShieldAlt className="text-xl text-primary" />
|
||||||
height={ 100 }
|
</div>
|
||||||
width={ 100 }
|
)}
|
||||||
priority
|
{sessionData.user.isVIP && (
|
||||||
/>
|
<div className="tooltip tooltip-secondary" data-tip="VIP">
|
||||||
</div>
|
<GiStarFormation className="inline-block text-xl text-secondary" />
|
||||||
) }
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{ providersLoading ? (
|
{providersLoading ? (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<span className="loading loading-dots loading-lg"></span>{ " " }
|
<span className="loading loading-dots loading-lg"></span>{" "}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<button
|
<button
|
||||||
className={ `btn btn-square btn-outline mx-2` }
|
className={`btn btn-square btn-outline mx-2`}
|
||||||
disabled={ providers?.includes("github") }
|
disabled={providers?.includes("github")}
|
||||||
onClick={ () => void signIn("github") }
|
onClick={() => void signIn("github")}
|
||||||
>
|
>
|
||||||
<SiGithub />
|
<SiGithub />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={ `btn btn-square btn-outline mx-2` }
|
className={`btn btn-square btn-outline mx-2`}
|
||||||
disabled={ providers?.includes("google") }
|
disabled={providers?.includes("google")}
|
||||||
onClick={ () => void signIn("google") }
|
onClick={() => void signIn("google")}
|
||||||
>
|
>
|
||||||
<SiGoogle />
|
<SiGoogle />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) }
|
)}
|
||||||
|
|
||||||
{ sessionData.user.name && (
|
{sessionData.user.name && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
className="input input-bordered"
|
className="input input-bordered"
|
||||||
value={ nameText }
|
value={nameText}
|
||||||
onChange={ (event) => setNameText(event.target.value) }
|
onChange={(event) => setNameText(event.target.value)}
|
||||||
/>
|
/>
|
||||||
) }
|
)}
|
||||||
|
|
||||||
{ sessionData.user.email && (
|
{sessionData.user.email && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
className="input input-bordered"
|
className="input input-bordered"
|
||||||
value={ sessionData.user.email }
|
value={sessionData.user.email}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
) }
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={ () => void saveUser() }
|
onClick={() => void saveUser()}
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
>
|
>
|
||||||
Save Account
|
Save Account
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* <button className="btn btn-error">Delete Account</button> */ }
|
{/* <button className="btn btn-error">Delete Account</button> */}
|
||||||
|
|
||||||
<label htmlFor="delete-user-modal" className="btn btn-error">
|
<label htmlFor="delete-user-modal" className="btn btn-error">
|
||||||
Delete Account
|
Delete Account
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
IoReloadOutline,
|
IoReloadOutline,
|
||||||
IoSaveOutline,
|
IoSaveOutline,
|
||||||
} from "react-icons/io5";
|
} from "react-icons/io5";
|
||||||
|
import { GiStarFormation } from "react-icons/gi";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { getServerAuthSession } from "../../server/auth";
|
import { getServerAuthSession } from "../../server/auth";
|
||||||
|
@ -109,7 +110,8 @@ const RoomBody: React.FC = ({}) => {
|
||||||
name: sessionData?.user.name || "",
|
name: sessionData?.user.name || "",
|
||||||
image: sessionData?.user.image || "",
|
image: sessionData?.user.image || "",
|
||||||
client_id: sessionData?.user.id || "",
|
client_id: sessionData?.user.id || "",
|
||||||
role: sessionData?.user.role || "USER",
|
isAdmin: sessionData?.user.isAdmin || false,
|
||||||
|
isVIP: sessionData?.user.isVIP || false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -302,7 +304,7 @@ const RoomBody: React.FC = ({}) => {
|
||||||
|
|
||||||
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md mx-auto">
|
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md mx-auto">
|
||||||
{presenceItem.data.name}{" "}
|
{presenceItem.data.name}{" "}
|
||||||
{presenceItem.data.role === "ADMIN" && (
|
{presenceItem.data.isAdmin && (
|
||||||
<div
|
<div
|
||||||
className="tooltip tooltip-primary"
|
className="tooltip tooltip-primary"
|
||||||
data-tip="Admin"
|
data-tip="Admin"
|
||||||
|
@ -310,6 +312,14 @@ const RoomBody: React.FC = ({}) => {
|
||||||
<FaShieldAlt className="inline-block text-primary" />
|
<FaShieldAlt className="inline-block text-primary" />
|
||||||
</div>
|
</div>
|
||||||
)}{" "}
|
)}{" "}
|
||||||
|
{presenceItem.data.isVIP && (
|
||||||
|
<div
|
||||||
|
className="tooltip tooltip-secondary"
|
||||||
|
data-tip="VIP"
|
||||||
|
>
|
||||||
|
<GiStarFormation className="inline-block text-secondary" />
|
||||||
|
</div>
|
||||||
|
)}{" "}
|
||||||
{presenceItem.clientId === roomFromDb.userId && (
|
{presenceItem.clientId === roomFromDb.userId && (
|
||||||
<div
|
<div
|
||||||
className="tooltip tooltip-warning"
|
className="tooltip tooltip-warning"
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { z } from "zod";
|
||||||
import { Goodbye } from "~/components/templates/Goodbye";
|
import { Goodbye } from "~/components/templates/Goodbye";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
import type { Role } from "~/utils/types";
|
|
||||||
|
|
||||||
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
|
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
|
||||||
|
|
||||||
|
@ -54,7 +53,8 @@ export const userRouter = createTRPCRouter({
|
||||||
}[];
|
}[];
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
role: Role;
|
isAdmin: boolean;
|
||||||
|
isVIP: boolean;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
}[]
|
}[]
|
||||||
|
@ -72,7 +72,8 @@ export const userRouter = createTRPCRouter({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
role: true,
|
isAdmin: true,
|
||||||
|
isVIP: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
email: true,
|
email: true,
|
||||||
sessions: {
|
sessions: {
|
||||||
|
@ -103,7 +104,7 @@ export const userRouter = createTRPCRouter({
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
let user: User;
|
let user: User;
|
||||||
if (input?.userId && ctx.session.user.role === "ADMIN") {
|
if (input?.userId && ctx.session.user.isAdmin) {
|
||||||
user = await ctx.prisma.user.delete({
|
user = await ctx.prisma.user.delete({
|
||||||
where: {
|
where: {
|
||||||
id: input.userId,
|
id: input.userId,
|
||||||
|
@ -149,11 +150,11 @@ export const userRouter = createTRPCRouter({
|
||||||
|
|
||||||
return !!user;
|
return !!user;
|
||||||
}),
|
}),
|
||||||
setRole: protectedProcedure
|
setAdmin: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
role: z.union([z.literal("ADMIN"), z.literal("USER")]),
|
value: z.boolean(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
@ -162,7 +163,29 @@ export const userRouter = createTRPCRouter({
|
||||||
id: input.userId,
|
id: input.userId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
role: input.role,
|
isAdmin: input.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await invalidateCache(`kv_userlist_admin`);
|
||||||
|
|
||||||
|
return !!user;
|
||||||
|
}),
|
||||||
|
|
||||||
|
setVIP: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
value: z.boolean(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const user = await ctx.prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: input.userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isVIP: input.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||||
import { type GetServerSidePropsContext } from "next";
|
import { type GetServerSidePropsContext } from "next";
|
||||||
import {
|
import {
|
||||||
getServerSession,
|
getServerSession,
|
||||||
type DefaultSession,
|
type DefaultSession,
|
||||||
type NextAuthOptions,
|
type NextAuthOptions,
|
||||||
} from "next-auth";
|
} from "next-auth";
|
||||||
import GithubProvider from "next-auth/providers/github";
|
import GithubProvider from "next-auth/providers/github";
|
||||||
import GoogleProvider from "next-auth/providers/google";
|
import GoogleProvider from "next-auth/providers/google";
|
||||||
import { Resend } from "resend";
|
import { Resend } from "resend";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import type { Role } from "~/utils/types";
|
|
||||||
import { Welcome } from "../components/templates/Welcome";
|
import { Welcome } from "../components/templates/Welcome";
|
||||||
import { invalidateCache } from "./redis";
|
import { invalidateCache } from "./redis";
|
||||||
|
|
||||||
|
@ -26,12 +25,14 @@ declare module "next-auth" {
|
||||||
interface Session extends DefaultSession {
|
interface Session extends DefaultSession {
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
role: Role;
|
isAdmin: boolean;
|
||||||
|
isVIP: boolean;
|
||||||
} & DefaultSession["user"];
|
} & DefaultSession["user"];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
role: Role;
|
isAdmin: boolean;
|
||||||
|
isVIP: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +46,8 @@ export const authOptions: NextAuthOptions = {
|
||||||
session({ session, user }) {
|
session({ session, user }) {
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
session.user.id = user.id;
|
session.user.id = user.id;
|
||||||
session.user.role = user.role;
|
session.user.isAdmin = user.isAdmin;
|
||||||
|
session.user.isVIP = user.isVIP;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,15 +7,10 @@ const EventTypes = {
|
||||||
} as const;
|
} as const;
|
||||||
export type EventType = BetterEnum<typeof EventTypes>;
|
export type EventType = BetterEnum<typeof EventTypes>;
|
||||||
|
|
||||||
const RoleValues = {
|
|
||||||
ADMIN: "ADMIN",
|
|
||||||
USER: "USER",
|
|
||||||
} as const;
|
|
||||||
export type Role = BetterEnum<typeof RoleValues>;
|
|
||||||
|
|
||||||
export interface PresenceItem {
|
export interface PresenceItem {
|
||||||
name: string;
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
client_id: string;
|
client_id: string;
|
||||||
role: Role;
|
isAdmin: boolean;
|
||||||
|
isVIP: boolean;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue