Skip to content

Commit

Permalink
block most users from being admins
Browse files Browse the repository at this point in the history
  • Loading branch information
epicdragon44 committed Sep 16, 2023
1 parent d5d3a57 commit 600ba3c
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 10 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ Simple serverless Slack bot for DTI Coffee Chats and Birthday messages!

Should pair up people in the `#coffee-chats` channel every week.

## Functionality

If you're a regular member, you should only be able to login, edit your own birthday, or report on whether the bot is up or down.

If you're an admin, you should be able to login, edit your own birthday, edit other people's birthdays, toggle the bot on or off, and also manually initiate a new semester, or trigger a round of coffee chats or birthday wishes as needed.

## New to Internbot?

If you're a new Lead, make a PR to add yourself to the array of admins in `lib/data/admins.ts` and merge it in. Then, login with Google and you should have access to the admin panel!

If you're a regular member, just login with Google.

For **everyone**, please login with your Cornell email.

## Technology

Built with Vercel, PostgreSQL, Prisma, Next.js, Jest, and TypeScript.
Expand Down Expand Up @@ -59,6 +73,6 @@ pnpm deploy # Deploy to Vercel

- [x] Polish up the UI.
- [ ] Allow admins to bulk edit user birthdays, activity, etc.
- [ ] Add a separate model for Admins that's separate from All-Members-in-Slack. Block access to gated actions unless specifically an Admin. Allow old Admins to set new Admins.
- [ ] Add a separate model for Admins that's separate from All-Members-in-Slack, instead of keeping admins in a data file. Block access to gated actions unless specifically an Admin. Allow old Admins to set new Admins.
- [ ] Add migration scripts for clearing out old semesters, schema changes, etc.
- [ ] Add remaining cronjob in `oldvercel.json` to `vercel.json`.
12 changes: 3 additions & 9 deletions app/(controls)/@gated/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { currentUser } from "@clerk/nextjs";
import prisma from "../../../lib/clients/prisma";
import Help from "@/components/ui/help";
import ToggleBot from "@/components/actions/toggle-bot";
import InitSem from "@/components/actions/init-sem";
import ManualTriggers from "@/components/actions/manual-triggers";
import SetSomeonesBday from "@/components/actions/set-someones-bday";
import admins from "@/lib/data/admins";

const GatedActions = async () => {
const hasAccess = await getHasDashboardAccess();
Expand All @@ -15,7 +15,7 @@ const GatedActions = async () => {
<span className='text-gray-400 border-gray-200 p-2 border-2 rounded-lg'>
Some actions are hidden.
</span>
<Help text='You do not have admin access. Your email was not found in the Cornell Slack database.' />
<Help text='You do not have admin access.' />
</div>
);
}
Expand All @@ -32,13 +32,7 @@ const GatedActions = async () => {

const getHasDashboardAccess = async () => {
const checkIfUserHasAccess = async (email: string) =>
(await prisma.user.findFirst({
where: {
email: email,
},
}))
? true
: false;
admins.some((admin) => admin.email === email);

const emails = (await currentUser())?.emailAddresses;

Expand Down
16 changes: 16 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";

const handler = NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
// ...add more providers here
],
});

export { handler as GET, handler as POST };
36 changes: 36 additions & 0 deletions components/expanding-arrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export default function ExpandingArrow({ className }: { className?: string }) {
return (
<div className="group relative flex items-center">
<svg
className={`${
className ? className : 'h-4 w-4'
} absolute transition-all group-hover:translate-x-1 group-hover:opacity-0`}
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 16 16"
width="16"
height="16"
>
<path
fillRule="evenodd"
d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"
></path>
</svg>
<svg
className={`${
className ? className : 'h-4 w-4'
} absolute opacity-0 transition-all group-hover:translate-x-1 group-hover:opacity-100`}
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 16 16"
width="16"
height="16"
>
<path
fillRule="evenodd"
d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"
></path>
</svg>
</div>
)
}
16 changes: 16 additions & 0 deletions components/gated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Suspense } from "react";
import BotStatus from "./gated/bot-status";
import SendCoffeeChats from "./gated/send-coffee-chats";
import AddBirthday from "./gated/add-birthday";

const GatedComponents = () => {
return (
<>
<BotStatus />
<SendCoffeeChats />
<AddBirthday />
</>
);
};

export default GatedComponents;
26 changes: 26 additions & 0 deletions components/gated/add-birthday.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { addUserOrUpdateBirthday } from "@/core/add-birthday";

const AddBirthday = () => {
const action = async (formdata: FormData) => {
"use server";

const email = formdata.get("email") as string | null;
const date = formdata.get("date") as string | null; // format: YYYY-MM-DD

if (!email || !date) {
return;
}

await addUserOrUpdateBirthday(email, new Date(date));
};

return (
<form action={action}>
<input type='email' name='email' required />
<input type='date' name='date' required />
<button type='submit'>Submit New Birthday</button>
</form>
);
};

export default AddBirthday;
18 changes: 18 additions & 0 deletions components/gated/bot-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { toggleCurrentBotStatus } from "@/core/bot-status";

// Server action defined inside a Server Component
const ToggleBotStatus = () => {
const toggle = async () => {
"use server";
await toggleCurrentBotStatus();
};

return (
// @ts-expect-error
<form action={toggle}>
<button type='submit'>Toggle Bot Enabled/Disabled</button>
</form>
);
};

export default ToggleBotStatus;
17 changes: 17 additions & 0 deletions components/gated/send-coffee-chats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { exec } from "@/core/send-coffee-chats";

const SendCoffeeChats = () => {
const action = async () => {
"use server";
await exec();
};

return (
// @ts-expect-error
<form action={action}>
<button type='submit'>Manually send a round of coffee chats</button>
</form>
);
};

export default SendCoffeeChats;
17 changes: 17 additions & 0 deletions components/non-gated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Suspense } from "react";
import BotStatus from "./non-gated/bot-status";
import PopulateNewSemester from "./non-gated/new-semester";

const NonGatedComponents = () => {
return (
<>
<Suspense fallback={<div>Fetching Bot Status...</div>}>
{/* @ts-expect-error Async Server Component */}
<BotStatus />
</Suspense>
<PopulateNewSemester />
</>
);
};

export default NonGatedComponents;
16 changes: 16 additions & 0 deletions components/non-gated/new-semester.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { exec } from "@/core/new-semester";

export default function PopulateNewSemester() {
async function action() {
"use server";
console.log("Populating new semester...");
await exec();
}

return (
// @ts-expect-error
<form action={action}>
<button type='submit'>Populate Database</button>
</form>
);
}
25 changes: 25 additions & 0 deletions components/refresh-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client'

import { useRouter } from 'next/navigation'
import { useTransition } from 'react'

export default function RefreshButton() {
const router = useRouter()
const [isPending, startTransition] = useTransition()

return (
<button
className={`${
isPending ? 'cursor-not-allowed text-gray-400' : ''
} text-sm text-gray-500 hover:text-gray-900`}
disabled={isPending}
onClick={() => {
startTransition(() => {
router.refresh()
})
}}
>
{isPending ? 'Refreshing...' : 'Refresh'}
</button>
)
}
29 changes: 29 additions & 0 deletions components/table-placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import RefreshButton from './refresh-button'

export default function TablePlaceholder() {
return (
<div className="bg-white/30 p-12 shadow-xl ring-1 ring-gray-900/5 rounded-lg backdrop-blur-lg max-w-xl mx-auto w-full">
<div className="flex justify-between items-center mb-4">
<div className="space-y-1">
<h2 className="text-xl font-semibold">Recent Users</h2>
<p className="text-sm text-gray-500">Fetching users...</p>
</div>
<RefreshButton />
</div>
<div className="divide-y divide-gray-900/5">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between py-3">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 rounded-full bg-gray-200 animate-pulse" />
<div className="space-y-1">
<div className="h-6 w-28 rounded-md bg-gray-200 animate-pulse" />
<div className="h-4 w-24 rounded-md bg-gray-200 animate-pulse" />
</div>
</div>
<div className="h-4 w-12 rounded-md bg-gray-200 animate-pulse" />
</div>
))}
</div>
</div>
)
}
47 changes: 47 additions & 0 deletions components/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import prisma from '@/lib/prisma'
import { timeAgo } from '@/lib/utils'
import Image from 'next/image'
import RefreshButton from './refresh-button'

export default async function Table() {
const startTime = Date.now()
const users = await prisma.users.findMany()
const duration = Date.now() - startTime

return (
<div className="bg-white/30 p-12 shadow-xl ring-1 ring-gray-900/5 rounded-lg backdrop-blur-lg max-w-xl mx-auto w-full">
<div className="flex justify-between items-center mb-4">
<div className="space-y-1">
<h2 className="text-xl font-semibold">Recent Users</h2>
<p className="text-sm text-gray-500">
Fetched {users.length} users in {duration}ms
</p>
</div>
<RefreshButton />
</div>
<div className="divide-y divide-gray-900/5">
{users.map((user) => (
<div
key={user.name}
className="flex items-center justify-between py-3"
>
<div className="flex items-center space-x-4">
<Image
src={user.image}
alt={user.name}
width={48}
height={48}
className="rounded-full ring-1 ring-gray-900/5"
/>
<div className="space-y-1">
<p className="font-medium leading-none">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
</div>
<p className="text-sm text-gray-500">{timeAgo(user.createdAt)}</p>
</div>
))}
</div>
</div>
)
}
33 changes: 33 additions & 0 deletions lib/data/admins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type admin = {
name?: string;
email: string;
};

const admins: admin[] = [
{
name: "Daniel Wei",
email: "dlw266@cornell.edu",
},
{
name: "Justin Kong",
email: "jk2338@cornell.edu",
},
{
name: "Julie Jeong",
email: "sj598@cornell.edu",
},
{
name: "Rohan Maheshwari",
email: "rm697@cornell.edu",
},
{
name: "Sarah Young",
email: "sy398@cornell.edu",
},
{
name: "Michelle Li",
email: "myl39@cornell.edu",
},
];

export default admins;
18 changes: 18 additions & 0 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import prisma from "../lib/prisma";

export async function main() {
const response = await Promise.all([
// Populate initial...
]);
console.log(response);
}

main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

0 comments on commit 600ba3c

Please sign in to comment.