Merge pull request #30 from atridadl/dev

Role changes
This commit is contained in:
Atridad Lahiji 2023-07-29 21:46:42 -06:00 committed by GitHub
commit 8fb8f5140f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 127 deletions

View file

@ -1,8 +1,3 @@
enum RoleValue {
USER
ADMIN
}
generator client {
provider = "prisma-client-js"
}
@ -46,7 +41,6 @@ model Session {
model User {
id String @id @default(cuid())
role RoleValue @default(USER)
createdAt DateTime @default(now())
name String?
email String? @unique
@ -57,6 +51,9 @@ model User {
rooms Room[]
votes Vote[]
logs Log[]
isAdmin Boolean @default(false)
isVIP Boolean @default(false)
}
model VerificationToken {

View file

@ -85,7 +85,7 @@ const Navbar: React.FC<NavbarProps> = ({ title }) => {
Profile
</Link>
</li>
{sessionData.user.role === "ADMIN" && (
{sessionData.user.isAdmin && (
<li>
<Link
about="Admin Page"

View file

@ -5,8 +5,8 @@ import { AiOutlineClear } from "react-icons/ai";
import { FaShieldAlt } from "react-icons/fa";
import { IoTrashBinOutline } from "react-icons/io5";
import { SiGithub, SiGoogle } from "react-icons/si";
import { GiStarFormation } from "react-icons/gi";
import { api } from "~/utils/api";
import type { Role } from "~/utils/types";
import { getServerAuthSession } from "../../server/auth";
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;
return {
redirect: {
@ -90,7 +90,8 @@ const AdminBody: React.FC = () => {
id: string;
}[];
id: string;
role: Role;
isAdmin: boolean;
isVIP: boolean;
name: 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 () => {
await refetchData();
},
@ -137,8 +144,12 @@ const AdminBody: React.FC = () => {
await clearSessionsMutation.mutateAsync();
};
const setUserRoleHandler = async (userId: string, role: Role) => {
await setRoleMutation.mutateAsync({ userId, role });
const setAdmin = async (userId: string, value: boolean) => {
await setAdminMutation.mutateAsync({ userId, value });
};
const setVIP = async (userId: string, value: boolean) => {
await setVIPMutation.mutateAsync({ userId, value });
};
const refetchData = async () => {
@ -158,70 +169,70 @@ const AdminBody: React.FC = () => {
<div className="stat">
<div className="stat-title">Users</div>
<div className="stat-value">
{ usersCountLoading || usersCountFetching ? (
{usersCountLoading || usersCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<>{ usersCount ? usersCount : "0" }</>
) }
<>{usersCount ? usersCount : "0"}</>
)}
</div>
</div>
<div className="stat">
<div className="stat-title">Rooms</div>
<div className="stat-value">
{ roomsCountLoading || roomsCountFetching ? (
{roomsCountLoading || roomsCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<>{ roomsCount ? roomsCount : "0" }</>
) }
<>{roomsCount ? roomsCount : "0"}</>
)}
</div>
</div>
<div className="stat">
<div className="stat-title">Votes</div>
<div className="stat-value">
{ votesCountLoading || votesCountFetching ? (
{votesCountLoading || votesCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<>{ votesCount ? votesCount : "0" }</>
) }
<>{votesCount ? votesCount : "0"}</>
)}
</div>
</div>
</div>
{ usersCountFetching ||
usersFetching ||
roomsCountFetching ||
votesCountFetching ? (
{usersCountFetching ||
usersFetching ||
roomsCountFetching ||
votesCountFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
<button
className="btn btn-primary m-2"
onClick={ () => void clearSessionsHandler() }
onClick={() => void clearSessionsHandler()}
>
Delete All Sessions
</button>
<button
className="btn btn-primary"
onClick={ () => void refetchData() }
onClick={() => void refetchData()}
>
Re-fetch
</button>
</div>
) }
)}
<div className="card max-w-[80vw] bg-neutral shadow-xl m-4">
<div className="card-body">
<h2 className="card-title">Users:</h2>
{ usersLoading || usersFetching ? (
{usersLoading || usersFetching ? (
<span className="loading loading-dots loading-lg"></span>
) : (
<div className="overflow-x-scroll">
<table className="table text-center">
{/* head */ }
{/* head */}
<thead>
<tr className="border-white">
<th>ID</th>
@ -233,55 +244,64 @@ const AdminBody: React.FC = () => {
</tr>
</thead>
<tbody className="">
{ users
{users
?.sort((user1, user2) =>
user2.createdAt > user1.createdAt ? 1 : -1
)
.map((user) => {
return (
<tr key={ user.id } className="hover">
<tr key={user.id} className="hover">
<td className="max-w-[100px] break-words">
{ user.id }
{user.id}
</td>
<td className="max-w-[100px] break-normal">
{ user.name }
{user.name}
</td>
<td className="max-w-[100px] break-normal">
{ user.createdAt.toLocaleDateString() }
{user.createdAt.toLocaleDateString()}
</td>
<td className="max-w-[100px] break-normal">
{ user.sessions.length }
{user.sessions.length}
</td>
<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" />
) }
{ getProviders(user).includes("github") && (
)}
{getProviders(user).includes("github") && (
<SiGithub className="text-xl m-1 inline-block hover:text-secondary" />
) }
)}
</td>
<td>
<button className="m-2">
{ user.role === "ADMIN" ? (
{user.isAdmin ? (
<FaShieldAlt
className="text-xl inline-block text-primary"
onClick={ () =>
void setUserRoleHandler(user.id, "USER")
}
onClick={() => void setAdmin(user.id, false)}
/>
) : (
<FaShieldAlt
className="text-xl inline-block"
onClick={ () =>
void setUserRoleHandler(user.id, "ADMIN")
}
onClick={() => void setAdmin(user.id, true)}
/>
) }
)}
</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
className="m-2"
onClick={ () =>
onClick={() =>
void clearSessionsByUserHandler(user.id)
}
>
@ -289,18 +309,18 @@ const AdminBody: React.FC = () => {
</button>
<button
className="m-2"
onClick={ () => void deleteUserHandler(user.id) }
onClick={() => void deleteUserHandler(user.id)}
>
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
</button>
</td>
</tr>
);
}) }
})}
</tbody>
</table>
</div>
) }
)}
</div>
</div>
</>

View file

@ -8,6 +8,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import { FaShieldAlt } from "react-icons/fa";
import { getServerAuthSession } from "~/server/auth";
import { GiStarFormation } from "react-icons/gi";
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
@ -57,20 +58,23 @@ const HomePageBody: React.FC = () => {
return (
<>
<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 }!{ " " }
{ sessionData?.user.role === "ADMIN" && (
Hi, {sessionData?.user.name}!{" "}
{sessionData?.user.isAdmin && (
<FaShieldAlt className="inline-block text-primary" />
) }
)}
{sessionData?.user.isVIP && (
<GiStarFormation className="inline-block text-secondary" />
)}
</h1>
<div className="tabs tabs-boxed border-2 border-cyan-500 mb-4">
<a
className={
tabIndex === 0 ? "tab no-underline tab-active" : "tab no-underline"
}
onClick={ () => {
onClick={() => {
setTabIndex(0);
localStorage.setItem("dashboardTabIndex", "0");
} }
}}
>
Join a Room
</a>
@ -78,36 +82,36 @@ const HomePageBody: React.FC = () => {
className={
tabIndex === 1 ? "tab no-underline tab-active" : "tab no-underline"
}
onClick={ () => {
onClick={() => {
setTabIndex(1);
localStorage.setItem("dashboardTabIndex", "1");
} }
}}
>
Room List
</a>
</div>
{ tabIndex === 0 && (
{tabIndex === 0 && (
<>
<input
type="text"
placeholder="Enter Room ID"
className="input input-bordered input-primary mb-4"
onChange={ (event) => {
onChange={(event) => {
console.log(event.target.value);
setJoinRoomTextBox(event.target.value);
} }
}}
/>
<Link
href={ joinRoomTextBox.length > 0 ? `/room/${joinRoomTextBox}` : "/" }
href={joinRoomTextBox.length > 0 ? `/room/${joinRoomTextBox}` : "/"}
className="btn btn-secondary"
>
Join Room
</Link>
</>
) }
)}
{ tabIndex === 1 && <RoomList /> }
{tabIndex === 1 && <RoomList />}
</>
);
};

View file

@ -9,6 +9,7 @@ import { FaShieldAlt } from "react-icons/fa";
import { SiGithub, SiGoogle } from "react-icons/si";
import { api } from "~/utils/api";
import { getServerAuthSession } from "../../server/auth";
import { GiStarFormation } from "react-icons/gi";
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
@ -103,7 +104,7 @@ const ProfileBody: React.FC = () => {
<label
htmlFor="delete-user-modal"
className="btn btn-error"
onClick={ () => void deleteCurrentUser() }
onClick={() => void deleteCurrentUser()}
>
I am sure!
</label>
@ -114,79 +115,82 @@ const ProfileBody: React.FC = () => {
<div className="card w-90 bg-neutral shadow-xl">
<div className="card-body">
<h2 className="card-title">Profile:</h2>
{ sessionData.user.image && (
<div className="indicator mx-auto m-4">
{ sessionData.user.role === "ADMIN" && (
<span className="indicator-item indicator-bottom badge badge-primary">
<div className="tooltip tooltip-primary" data-tip="Admin">
<FaShieldAlt className="text-xl" />
</div>
</span>
) }
{sessionData.user.image && (
<Image
className="mx-auto"
src={sessionData.user.image}
alt="Profile picture."
height={100}
width={100}
priority
/>
)}
<Image
className="mx-auto"
src={ sessionData.user.image }
alt="Profile picture."
height={ 100 }
width={ 100 }
priority
/>
</div>
) }
<div className="flex flex-row flex-wrap items-center text-center justify-center gap-4">
{sessionData.user.isAdmin && (
<div className="tooltip tooltip-primary" data-tip="Admin">
<FaShieldAlt className="text-xl text-primary" />
</div>
)}
{sessionData.user.isVIP && (
<div className="tooltip tooltip-secondary" data-tip="VIP">
<GiStarFormation className="inline-block text-xl text-secondary" />
</div>
)}
</div>
{ providersLoading ? (
{providersLoading ? (
<div className="mx-auto">
<span className="loading loading-dots loading-lg"></span>{ " " }
<span className="loading loading-dots loading-lg"></span>{" "}
</div>
) : (
<div className="mx-auto">
<button
className={ `btn btn-square btn-outline mx-2` }
disabled={ providers?.includes("github") }
onClick={ () => void signIn("github") }
className={`btn btn-square btn-outline mx-2`}
disabled={providers?.includes("github")}
onClick={() => void signIn("github")}
>
<SiGithub />
</button>
<button
className={ `btn btn-square btn-outline mx-2` }
disabled={ providers?.includes("google") }
onClick={ () => void signIn("google") }
className={`btn btn-square btn-outline mx-2`}
disabled={providers?.includes("google")}
onClick={() => void signIn("google")}
>
<SiGoogle />
</button>
</div>
) }
)}
{ sessionData.user.name && (
{sessionData.user.name && (
<input
type="text"
placeholder="Name"
className="input input-bordered"
value={ nameText }
onChange={ (event) => setNameText(event.target.value) }
value={nameText}
onChange={(event) => setNameText(event.target.value)}
/>
) }
)}
{ sessionData.user.email && (
{sessionData.user.email && (
<input
type="text"
placeholder="Email"
className="input input-bordered"
value={ sessionData.user.email }
value={sessionData.user.email}
disabled
/>
) }
)}
<button
onClick={ () => void saveUser() }
onClick={() => void saveUser()}
className="btn btn-secondary"
>
Save Account
</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">
Delete Account

View file

@ -15,6 +15,7 @@ import {
IoReloadOutline,
IoSaveOutline,
} from "react-icons/io5";
import { GiStarFormation } from "react-icons/gi";
import { z } from "zod";
import { api } from "~/utils/api";
import { getServerAuthSession } from "../../server/auth";
@ -109,7 +110,8 @@ const RoomBody: React.FC = ({}) => {
name: sessionData?.user.name || "",
image: sessionData?.user.image || "",
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">
{presenceItem.data.name}{" "}
{presenceItem.data.role === "ADMIN" && (
{presenceItem.data.isAdmin && (
<div
className="tooltip tooltip-primary"
data-tip="Admin"
@ -310,6 +312,14 @@ const RoomBody: React.FC = ({}) => {
<FaShieldAlt className="inline-block text-primary" />
</div>
)}{" "}
{presenceItem.data.isVIP && (
<div
className="tooltip tooltip-secondary"
data-tip="VIP"
>
<GiStarFormation className="inline-block text-secondary" />
</div>
)}{" "}
{presenceItem.clientId === roomFromDb.userId && (
<div
className="tooltip tooltip-warning"

View file

@ -4,7 +4,6 @@ import { z } from "zod";
import { Goodbye } from "~/components/templates/Goodbye";
import { env } from "~/env.mjs";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import type { Role } from "~/utils/types";
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
@ -54,7 +53,8 @@ export const userRouter = createTRPCRouter({
}[];
id: string;
createdAt: Date;
role: Role;
isAdmin: boolean;
isVIP: boolean;
name: string | null;
email: string | null;
}[]
@ -72,7 +72,8 @@ export const userRouter = createTRPCRouter({
select: {
id: true,
name: true,
role: true,
isAdmin: true,
isVIP: true,
createdAt: true,
email: true,
sessions: {
@ -103,7 +104,7 @@ export const userRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
let user: User;
if (input?.userId && ctx.session.user.role === "ADMIN") {
if (input?.userId && ctx.session.user.isAdmin) {
user = await ctx.prisma.user.delete({
where: {
id: input.userId,
@ -149,11 +150,11 @@ export const userRouter = createTRPCRouter({
return !!user;
}),
setRole: protectedProcedure
setAdmin: protectedProcedure
.input(
z.object({
userId: z.string(),
role: z.union([z.literal("ADMIN"), z.literal("USER")]),
value: z.boolean(),
})
)
.mutation(async ({ ctx, input }) => {
@ -162,7 +163,29 @@ export const userRouter = createTRPCRouter({
id: input.userId,
},
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 { type GetServerSidePropsContext } from "next";
import {
getServerSession,
type DefaultSession,
type NextAuthOptions,
getServerSession,
type DefaultSession,
type NextAuthOptions,
} from "next-auth";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import { Resend } from "resend";
import { env } from "~/env.mjs";
import { prisma } from "~/server/db";
import type { Role } from "~/utils/types";
import { Welcome } from "../components/templates/Welcome";
import { invalidateCache } from "./redis";
@ -26,12 +25,14 @@ declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
role: Role;
isAdmin: boolean;
isVIP: boolean;
} & DefaultSession["user"];
}
interface User {
role: Role;
isAdmin: boolean;
isVIP: boolean;
}
}
@ -45,7 +46,8 @@ export const authOptions: NextAuthOptions = {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
session.user.role = user.role;
session.user.isAdmin = user.isAdmin;
session.user.isVIP = user.isVIP;
}
return session;
},

View file

@ -7,15 +7,10 @@ const EventTypes = {
} as const;
export type EventType = BetterEnum<typeof EventTypes>;
const RoleValues = {
ADMIN: "ADMIN",
USER: "USER",
} as const;
export type Role = BetterEnum<typeof RoleValues>;
export interface PresenceItem {
name: string;
image: string;
client_id: string;
role: Role;
isAdmin: boolean;
isVIP: boolean;
}