forked from dubinc/dub
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added admin panel, moved login/register page to App Router, fixed rob…
…ots.txt
- Loading branch information
1 parent
a75eaaf
commit df92964
Showing
36 changed files
with
895 additions
and
335 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from "../../app/(auth)/login/page"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.