Skip to content

Commit

Permalink
Added admin panel, moved login/register page to App Router, fixed rob…
Browse files Browse the repository at this point in the history
…ots.txt
  • Loading branch information
steven-tey committed Jul 19, 2023
1 parent a75eaaf commit df92964
Show file tree
Hide file tree
Showing 36 changed files with 895 additions and 335 deletions.
199 changes: 199 additions & 0 deletions app/admin/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"use server";
import { deleteUserLinks } from "#/lib/api/links";
import { hashToken } from "#/lib/auth";
import { DUB_PROJECT_ID } from "#/lib/constants";
import prisma from "#/lib/prisma";
import { getDomainWithoutWWW } from "#/lib/utils";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import { get } from "@vercel/edge-config";
import { randomBytes } from "crypto";
import { getServerSession } from "next-auth";

async function isAdmin() {
const session = await getServerSession(authOptions);
if (!session?.user) return false;
const response = await prisma.projectUsers.findUnique({
where: {
userId_projectId: {
// @ts-ignore
userId: session.user.id,
projectId: DUB_PROJECT_ID,
},
},
});
if (!response) return false;
return true;
}

export async function getData(data: FormData) {
const key = data.get("key") as string;

if (!(await isAdmin())) {
return {
error: "Unauthorized",
};
}

const response = await prisma.user.findFirst({
where: {
links: {
some: {
domain: "dub.sh",
key,
},
},
},
select: {
email: true,
links: {
where: {
domain: "dub.sh",
},
select: {
key: true,
url: true,
},
},
projects: {
where: {
role: "owner",
},
select: {
project: {
select: {
name: true,
slug: true,
domains: {
select: {
slug: true,
verified: true,
},
},
},
},
},
},
},
});
if (!response?.email) {
return {
error: "No user found",
};
}

const { email, links, projects } = response;

const hostnames = new Set<string>();

links.map((link) => {
const hostname = getDomainWithoutWWW(link.url);
hostname && hostnames.add(hostname);
});

const verifiedDomains = projects
.filter(({ project }) => {
return project.domains.some(({ verified }) => verified);
})
.flatMap(({ project }) => project.domains.map(({ slug }) => slug));

const token = randomBytes(32).toString("hex");

await prisma.verificationToken.create({
data: {
identifier: email,
token: hashToken(token),
expires: new Date(Date.now() + 60000),
},
});

const params = new URLSearchParams({
callbackUrl: process.env.NEXTAUTH_URL as string,
email,
token,
});

const url = `${process.env.NEXTAUTH_URL}/api/auth/callback/email?${params}`;

return {
email: response?.email as string,
hostnames: Array.from(hostnames),
verifiedDomains: verifiedDomains || [],
impersonateUrl: url,
};
}

export async function banUser(data: FormData) {
const email = data.get("email") as string;
const hostnames = data.getAll("hostname") as string[];

if (!(await isAdmin())) {
return {
error: "Unauthorized",
};
}

const user = await prisma.user.findUnique({
where: {
email,
},
select: {
id: true,
},
});

if (!user) {
return {
error: "No user found",
};
}

const blacklistedDomains = await get("domains");
const blacklistedEmails = await get("emails");

const ban = await Promise.allSettled([
deleteUserLinks(user.id),
fetch(
`https://api.vercel.com/v1/edge-config/${process.env.EDGE_CONFIG_ID}/items?teamId=${process.env.TEAM_ID_VERCEL}`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [
{
operation: "update",
key: "domains",
value: [...blacklistedDomains, ...hostnames],
},
{
operation: "update",
key: "emails",
value: [...blacklistedEmails, email],
},
],
}),
},
).then((res) => res.json()),
]);

const response = await prisma.user.delete({
where: {
id: user.id,
},
});

console.log(
JSON.stringify(
{
ban,
response,
},
null,
2,
),
);

return true;
}
15 changes: 15 additions & 0 deletions app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ReactNode } from "react";
import Background from "#/ui/home/background";

export default async function AdminLayout({
children,
}: {
children: ReactNode;
}) {
return (
<div className="flex h-screen w-screen justify-center">
<Background />
{children}
</div>
);
}
1 change: 1 addition & 0 deletions app/admin/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "../../app/(auth)/login/page";
14 changes: 14 additions & 0 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { constructMetadata } from "#/lib/utils";
import AdminStuff from "./stuff";

export const metadata = constructMetadata({
title: "Dub Admin",
});

export default function AdminPage() {
return (
<div className="mx-auto min-h-screen w-full max-w-screen-sm bg-white p-5">
<AdminStuff />
</div>
);
}
35 changes: 35 additions & 0 deletions app/admin/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { redirect } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { getServerSession } from "next-auth";
import { authOptions } from "@/pages/api/auth/[...nextauth]";

export default async function Profile() {
const session = await getServerSession(authOptions);
if (!session?.user) {
redirect("/login");
}

return (
<div className="flex w-full items-center justify-between">
<Link
href="/settings"
className="flex w-full flex-1 items-center space-x-3 rounded-lg px-2 py-1.5 transition-all duration-150 ease-in-out hover:bg-stone-200 active:bg-stone-300 dark:text-white dark:hover:bg-stone-700 dark:active:bg-stone-800"
>
<Image
src={
session.user.image ??
`https://avatar.vercel.sh/${session.user.email}`
}
width={40}
height={40}
alt={session.user.name ?? "User avatar"}
className="h-6 w-6 rounded-full"
/>
<span className="truncate text-sm font-medium">
{session.user.name}
</span>
</Link>
</div>
);
}
Loading

0 comments on commit df92964

Please sign in to comment.