Role overhaul

This commit is contained in:
Atridad Lahiji 2023-07-29 21:38:59 -06:00
parent c402732f0a
commit 95c9314d03
No known key found for this signature in database
GPG key ID: 7CB8245F56BC3880
8 changed files with 114 additions and 96 deletions

View file

@ -1,9 +1,3 @@
enum RoleValue {
USER
ADMIN
VIP
}
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
} }
@ -47,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
@ -58,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 {

View file

@ -7,7 +7,6 @@ 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 { 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) => {
@ -23,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: {
@ -91,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;
}) => { }) => {
@ -120,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();
}, },
@ -138,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 () => {
@ -264,36 +274,28 @@ const AdminBody: React.FC = () => {
</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>
<button className="m-2"> <button className="m-2">
{user.role === "VIP" ? ( {user.isVIP ? (
<GiStarFormation <GiStarFormation
className="text-xl inline-block text-secondary" className="text-xl inline-block text-secondary"
onClick={() => onClick={() => void setVIP(user.id, false)}
void setUserRoleHandler(user.id, "USER")
}
/> />
) : ( ) : (
<GiStarFormation <GiStarFormation
className="text-xl inline-block" className="text-xl inline-block"
onClick={() => onClick={() => void setVIP(user.id, true)}
void setUserRoleHandler(user.id, "VIP")
}
/> />
)} )}
</button> </button>

View file

@ -59,10 +59,10 @@ const HomePageBody: React.FC = () => {
<> <>
<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.role === "VIP" && ( {sessionData?.user.isVIP && (
<GiStarFormation className="inline-block text-secondary" /> <GiStarFormation className="inline-block text-secondary" />
)} )}
</h1> </h1>

View file

@ -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

View file

@ -110,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,
} }
); );
@ -303,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"
@ -311,7 +312,7 @@ const RoomBody: React.FC = ({}) => {
<FaShieldAlt className="inline-block text-primary" /> <FaShieldAlt className="inline-block text-primary" />
</div> </div>
)}{" "} )}{" "}
{presenceItem.data.role === "VIP" && ( {presenceItem.data.isVIP && (
<div <div
className="tooltip tooltip-secondary" className="tooltip tooltip-secondary"
data-tip="VIP" data-tip="VIP"

View file

@ -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,15 +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([ value: z.boolean(),
z.literal("ADMIN"),
z.literal("USER"),
z.literal("VIP"),
]),
}) })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@ -166,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,
}, },
}); });

View file

@ -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;
}, },

View file

@ -7,16 +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",
VIP: "VIP",
} 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;
} }