diff --git a/.env.local.example b/.env.local.example index 3de5f23..d75fd80 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,2 +1,26 @@ +# Connection URL for Redis REDIS= -MONGODB= \ No newline at end of file + +# Connection URL for MongoDB +MONGODB= + +# Authentication +AUTHENTIK_ID= +AUTHENTIK_SECRET= +AUTHENTIK_ISSUER=https://sso.revolt.chat/application/o/swiss-army-knife + +# Next Auth +NEXTAUTH_SECRET= +NEXTAUTH_URL=https://admin.revolt.chat + +# Web server +PORT=3000 + +# Configure push notifications +NTFY_SERVER=https://ntfy.revolt.wtf +NTFY_TOPIC=reports +NTFY_USERNAME=admin-panel +NTFY_PASSWORD= + +# Disable authentication and RBAC +# NEXT_PUBLIC_AUTH_TYPE=none diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ddf87ba --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "tabWidth": 2, + "useTabs": false, + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrder": [ + "", + "^@radix-ui", + "^\\.\\.", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true +} diff --git a/README.md b/README.md index e37f8d4..d4de22b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ ```bash +bun install bun run dev ``` production: ```bash +bun install bun run build bun run start ``` diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..97beaa6 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import { authOptions } from "@/lib/auth/serverConfig"; +import NextAuth from "next-auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..317070a --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { Card, Flex, Heading, Text, Theme } from "@radix-ui/themes"; + +import styles from "./home.module.css"; + +export default function Error({ error }: { error: Error }) { + return ( + +
+ + + + Internal Server Error + + {String(error)} + + +
+
+ ); +} diff --git a/app/home.module.css b/app/home.module.css new file mode 100644 index 0000000..dc08f22 --- /dev/null +++ b/app/home.module.css @@ -0,0 +1,8 @@ +.main { + backdrop-filter: blur(24px); + background: linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), + url("/maxim-berg-kE8-rUKjtQU-unsplash.jpg"); + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} diff --git a/app/layout.tsx b/app/layout.tsx index be8cc1d..5e222b5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,28 +1,17 @@ +import { ClientAuthProvider } from "@/lib/auth/clientProvider"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; -import "./globals.css"; +import { Theme } from "@radix-ui/themes"; import "@radix-ui/themes/styles.css"; -import { Avatar, Box, Button, Card, Flex, Text, Theme } from "@radix-ui/themes"; -import { - Cross2Icon, - GroupIcon, - HomeIcon, - InfoCircledIcon, - Link1Icon, - LockClosedIcon, - MagnifyingGlassIcon, - PersonIcon, - ReaderIcon, - TrashIcon, -} from "@radix-ui/react-icons"; -import Link from "next/link"; + +import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Revolt Admin Panel", - description: "Generated by create next app", + description: "Platform management and moderation tools.", }; export default function RootLayout({ @@ -31,109 +20,14 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - -
- - - - - - - - - insert@revolt.chat - - - Admin - - - - - - - - - - - - - - - - - - - - {/*
*/} - - {/* {[ - "Case: Server(s) ijghhjifg", - "Server: Balls!", - "User: userisreal", - ].map((x, i) => ( - - - - - ))} */} - -
-
+ + + + {children} - - - - + + + + ); } diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..d053ee0 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,23 @@ +import { Card, Flex, Heading, Theme } from "@radix-ui/themes"; + +import styles from "./home.module.css"; + +export default function NotFound() { + return ( + +
+ + + + Page Not Found + + + +
+
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index be27305..357db21 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,35 @@ -export default function A() { - return
ding
; +import { LoginButton } from "@/components/common/auth/LoginButton"; +import { Comic_Neue } from "next/font/google"; + +import { Card, Flex, Heading, Text } from "@radix-ui/themes"; + +import styles from "./home.module.css"; + +const comicNeue = Comic_Neue({ subsets: ["latin"], weight: "700" }); + +export default function Home() { + return ( +
+ + + + Revolt Admin Panel + + + + + + revolt.chat ·{" "} + + Project Information + + + + +
+ ); } diff --git a/app/panel/about/page.tsx b/app/panel/about/page.tsx new file mode 100644 index 0000000..f4151ba --- /dev/null +++ b/app/panel/about/page.tsx @@ -0,0 +1,26 @@ +import { PageTitle } from "@/components/common/navigation/PageTitle"; +import { Metadata } from "next"; + +import { Text } from "@radix-ui/themes"; + +import pkg from "../../../package.json"; + +export const metadata: Metadata = { + title: "About", + description: + "Version information and other useful tidbits about this software.", +}; + +export default async function About() { + return ( + <> + + + Version {pkg.version} ·{" "} + + Source code + + + + ); +} diff --git a/app/panel/layout.tsx b/app/panel/layout.tsx new file mode 100644 index 0000000..c9227a5 --- /dev/null +++ b/app/panel/layout.tsx @@ -0,0 +1,19 @@ +import { Sidebar } from "@/components/common/navigation/Sidebar"; + +import { Flex } from "@radix-ui/themes"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ +
+ + + {children} + +
+
+ ); +} diff --git a/app/panel/page.tsx b/app/panel/page.tsx new file mode 100644 index 0000000..80f6014 --- /dev/null +++ b/app/panel/page.tsx @@ -0,0 +1,18 @@ +import { PageTitle } from "@/components/common/navigation/PageTitle"; +import { Metadata } from "next"; + +import { Text } from "@radix-ui/themes"; + +export const metadata: Metadata = { + title: "Dashboard", + description: "View pending alerts and important metrics from one place.", +}; + +export default function Dashboard() { + return ( + <> + + many such cases... + + ); +} diff --git a/bun.lockb b/bun.lockb index 0ccdea3..38069d9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/common/auth/LoginButton.tsx b/components/common/auth/LoginButton.tsx new file mode 100644 index 0000000..6e03c01 --- /dev/null +++ b/components/common/auth/LoginButton.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useAuthorisedUser } from "@/lib/auth"; +import { signIn, signOut } from "next-auth/react"; +import Link from "next/link"; + +import { Button, Flex } from "@radix-ui/themes"; + +export function LoginButton() { + if (process.env.NEXT_PUBLIC_AUTH_TYPE === "none") { + return ( + + ); + } + + const user = useAuthorisedUser(true); + if (user) { + return ( + + + + + ); + } + + const callbackUrl = + typeof window !== "undefined" + ? new URLSearchParams(document.location.search).get("callbackUrl") ?? + undefined + : undefined; + + return ( + + ); +} diff --git a/components/common/navigation/AuthorisedUserCard.tsx b/components/common/navigation/AuthorisedUserCard.tsx new file mode 100644 index 0000000..1392d97 --- /dev/null +++ b/components/common/navigation/AuthorisedUserCard.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useAuthorisedUser } from "@/lib/auth"; +import { signOut } from "next-auth/react"; + +import { ExitIcon } from "@radix-ui/react-icons"; +import { Avatar, Box, Card, Flex, IconButton, Text } from "@radix-ui/themes"; + +export function AuthorisedUserCard() { + const { name, email, image, usingNextAuth } = useAuthorisedUser(); + + return ( + + + + + + {name} + + + {email} {/* or we can show top-most role */} + + + {usingNextAuth && ( + signOut()}> + + + )} + + + ); +} diff --git a/components/common/navigation/PageTitle.tsx b/components/common/navigation/PageTitle.tsx new file mode 100644 index 0000000..1ba9ba0 --- /dev/null +++ b/components/common/navigation/PageTitle.tsx @@ -0,0 +1,14 @@ +import { Metadata } from "next"; + +import { Flex, Heading } from "@radix-ui/themes"; + +/** + * Render the page title + */ +export function PageTitle({ metadata }: { metadata: Metadata }) { + return ( + + {metadata.title as string} + + ); +} diff --git a/components/common/navigation/Sidebar.tsx b/components/common/navigation/Sidebar.tsx new file mode 100644 index 0000000..9150a98 --- /dev/null +++ b/components/common/navigation/Sidebar.tsx @@ -0,0 +1,116 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +import { + ExitIcon, + GroupIcon, + HomeIcon, + InfoCircledIcon, + Link1Icon, + LockClosedIcon, + MagnifyingGlassIcon, + PersonIcon, + ReaderIcon, + TrashIcon, +} from "@radix-ui/react-icons"; +import { + Avatar, + Box, + Button, + Card, + Flex, + IconButton, + Text, +} from "@radix-ui/themes"; + +import { AuthorisedUserCard } from "./AuthorisedUserCard"; + +export function Sidebar() { + const pathname = usePathname(); + + return ( + + + + + + + {/* + + + + + */} + + + {/*
*/} + + {/* {[ + "Case: Server(s) ijghhjifg", + "Server: Balls!", + "User: userisreal", + ].map((x, i) => ( + + + + + ))} */} + + ); +} diff --git a/lib/auth/clientProvider.tsx b/lib/auth/clientProvider.tsx new file mode 100644 index 0000000..8b9691b --- /dev/null +++ b/lib/auth/clientProvider.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; + +type Props = { + children?: React.ReactNode; +}; + +export const ClientAuthProvider = ({ children }: Props) => { + return {children}; +}; diff --git a/lib/auth/index.ts b/lib/auth/index.ts new file mode 100644 index 0000000..9e63b20 --- /dev/null +++ b/lib/auth/index.ts @@ -0,0 +1,43 @@ +import { useSession } from "next-auth/react"; + +type AuthorisedUser = { + name: string; + email: string; + image: string; + usingNextAuth: boolean; +}; + +/** + * Use the currently authorised user + * @param allowNull Whether to allow a null user to be returned + * @returns User details + */ +export function useAuthorisedUser(allowNull = false): AuthorisedUser { + if (process.env.NEXT_PUBLIC_AUTH_TYPE === "none") { + return { + name: "Instance Owner", + email: "owner@example.com", + image: "/tmp/pfp.png", + usingNextAuth: false, + }; + } else { + const { data: session } = useSession(); + if (!session?.user?.email) { + if (allowNull) return null!; + + return { + name: "Fetching user...", + email: "first.last@example.com", + image: "/tmp/pfp.png", + usingNextAuth: true, + }; + } + + return { + name: session.user.name ?? session.user.email ?? "A User", + email: session.user.email, + image: session.user.image ?? "/tmp/pfp.png", + usingNextAuth: true, + }; + } +} diff --git a/lib/auth/serverConfig.ts b/lib/auth/serverConfig.ts new file mode 100644 index 0000000..38bf4f9 --- /dev/null +++ b/lib/auth/serverConfig.ts @@ -0,0 +1,21 @@ +import type { AuthOptions } from "next-auth"; +import AuthentikProvider from "next-auth/providers/authentik"; + +/** + * Authentication options + */ +export const authOptions: AuthOptions = { + providers: [ + AuthentikProvider({ + clientId: process.env.AUTHENTIK_ID!, + clientSecret: process.env.AUTHENTIK_SECRET!, + issuer: process.env.AUTHENTIK_ISSUER!, + }), + ], + jwt: { + maxAge: 2 * 60 * 60, // 2 hours + }, + pages: { + signIn: "/", + }, +}; diff --git a/middleware.js b/middleware.js new file mode 100644 index 0000000..a2bad88 --- /dev/null +++ b/middleware.js @@ -0,0 +1,8 @@ +export { default } from "next-auth/middleware"; + +export const config = { + matcher: ["/panel"], + pages: { + signIn: "/", + }, +}; diff --git a/package.json b/package.json index 8d576b5..0d682ec 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lru-cache": "^10.2.0", "mongodb": "^6.3.0", "next": "14.0.4", + "next-auth": "^4.24.5", "node-cron": "^3.0.3", "react": "^18", "react-dom": "^18", @@ -30,6 +31,7 @@ "ulid": "^2.3.0" }, "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/node": "^20", "@types/node-cron": "^3.0.11", "@types/react": "^18", @@ -39,6 +41,7 @@ "eslint-config-next": "14.0.4", "nodemon": "^3.0.3", "postcss": "^8", + "prettier": "^3.2.5", "tailwindcss": "^3.3.0", "typescript": "^5" } diff --git a/public/ilgmyzin-hROoLloTfWQ-unsplash.jpg b/public/ilgmyzin-hROoLloTfWQ-unsplash.jpg new file mode 100644 index 0000000..9e8a68e Binary files /dev/null and b/public/ilgmyzin-hROoLloTfWQ-unsplash.jpg differ diff --git a/public/maxim-berg-kE8-rUKjtQU-unsplash.jpg b/public/maxim-berg-kE8-rUKjtQU-unsplash.jpg new file mode 100644 index 0000000..2cd751c Binary files /dev/null and b/public/maxim-berg-kE8-rUKjtQU-unsplash.jpg differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/tmp/pfp.png b/public/tmp/pfp.png new file mode 100644 index 0000000..5ff2259 Binary files /dev/null and b/public/tmp/pfp.png differ diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f8422..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/wide.svg b/public/wide.svg new file mode 100644 index 0000000..0f9b1d3 --- /dev/null +++ b/public/wide.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 8c181c4..c205ddd 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,19 @@ +import { + bgBlue, + bgGreen, + bgRed, + blue, + gray, + red, + yellow, +} from "@colors/colors"; +import { readFile, readdir } from "fs/promises"; import { createServer } from "http"; -import { parse } from "url"; import next from "next"; +import { resolve } from "path"; +import { parse } from "url"; + +import { createLogger } from "./logger"; const dev = process.env.NODE_ENV !== "production"; const hostname = process.env.HOST || "localhost"; @@ -9,14 +22,9 @@ const port = parseInt(process.env.PORT || "3000"); const app = next({ dev, hostname, port }); const handle = app.getRequestHandler(); -import { bgBlue, bgGreen, bgRed, gray, red } from "@colors/colors"; -import { readdir, readFile } from "fs/promises"; -import { resolve } from "path"; -import { createLogger } from "./logger"; - async function printVersion() { const { version } = JSON.parse( - await readFile("package.json").then((f) => f.toString()) + await readFile("package.json").then((f) => f.toString()), ); console.log("\n"); @@ -29,6 +37,7 @@ async function loadModules() { log(`Found ${modules.length} modules!`); for (const moduleName of modules) { + if (moduleName !== "bot-shield") continue; log(gray(`Initialising ${moduleName}`)); require(resolve(`server/.build/server/modules/${moduleName}/index.js`)); } @@ -56,7 +65,11 @@ async function startApp() { process.exit(1); }) .listen(port, () => - log(`Admin Panel is ready on http://${hostname}:${port}`) + log( + `${gray("Admin Panel is ready on http://")}${blue(hostname)}${gray( + ":", + )}${yellow(port.toString())}`, + ), ); }); }