diff --git a/Server/Source/Handlers/Server.ts b/Server/Source/Handlers/Server.ts index 044f208..60ca282 100644 --- a/Server/Source/Handlers/Server.ts +++ b/Server/Source/Handlers/Server.ts @@ -44,6 +44,11 @@ async function Initialize() { } App.use((_, res) => res.status(404).json({ errorMessage: "Not Found" })); + + App.use((err, _, res, __) => { + console.error(err); + res.status(500).json({ errorMessage: IS_DEBUG ? err : "Oops! Something broke on our end. Sorry!" }); + }) App.listen(PORT, () => Msg(`${magenta(PROJECT_NAME)} now up on port ${magenta(PORT)} ${(IS_DEBUG ? red("(debug environment)") : "")}`)); } diff --git a/Server/Source/Routes/Admin.ts b/Server/Source/Routes/Admin.ts index 9390c59..31b51f2 100644 --- a/Server/Source/Routes/Admin.ts +++ b/Server/Source/Routes/Admin.ts @@ -1,8 +1,12 @@ +/* eslint-disable no-case-declarations */ +import { FULL_SERVER_ROOT } from "../Modules/Constants"; import { Router } from "express"; -import { ADMIN_KEY, FULL_SERVER_ROOT } from "../Modules/Constants"; +import { UserPermissions } from "../Schemas/User"; import { Song } from "../Schemas/Song"; -import { ValidateBody } from "../Modules/Middleware"; +import { RequireAuthentication, ValidateBody } from "../Modules/Middleware"; import { writeFileSync } from "fs"; +import { ForcedCategory } from "../Schemas/ForcedCategory"; +import ffmpeg from "fluent-ffmpeg"; import exif from "exif-reader"; import j from "joi"; @@ -12,18 +16,19 @@ const App = Router(); // ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION ! // ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION ! -App.use((req, res, next) => { - if (req.path === "/key") - return res.status(req.body.Key === ADMIN_KEY ? 200 : 403).send(req.body.Key === ADMIN_KEY ? "Login successful!" : "Key doesn't match. Try again."); +App.use(RequireAuthentication()); - if ((req.cookies["AdminKey"] ?? req.header("Authorization")) !== ADMIN_KEY) +App.use((req, res, next) => { + const IsAdmin = req.user!.PermissionLevel! >= UserPermissions.Administrator; + if (req.path === "/key") + return res.status(IsAdmin ? 200 : 403).send(IsAdmin ? "Login successful!" : "Key doesn't match. Try again."); + + if (!IsAdmin) return res.status(403).send("You don't have permission to access this endpoint."); next(); }); -App.get("/test", (_, res) => res.send("Permission check OK")); - App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package()))); App.post("/create/song", @@ -68,6 +73,18 @@ async (req, res) => { res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`); }); +App.post("/upload/audio", +ValidateBody(j.object({ + Data: j.string().hex().required(), + TargetSong: j.string().uuid().required() +})), +async (req, res) => { + const Decoded = Buffer.from(req.body.Data, "hex"); + + if (!await Song.exists({ where: { ID: req.body.TargetSong } })) + return res.status(404).send("The song you're trying to upload audio for does not exist."); +}) + App.post("/upload/cover", ValidateBody(j.object({ Data: j.string().hex().required(), @@ -98,6 +115,51 @@ async (req, res) => { res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/cover.png`); }); +App.post("/update/discovery", +ValidateBody(j.array().items(j.object({ + ID: j.string().uuid().required(), + Songs: j.array().items(j.string().uuid()).unique().min(1).max(20).required(), + Priority: j.number().min(-50000).max(50000).required(), + Header: j.string().min(3).max(125).required(), + Action: j.string().valid("CREATE", "UPDATE", "DELETE").required() +})).max(100)), +async (req, res) => { + const b = req.body as { ID: string, Songs: string[], Priority: number, Header: string, Action: "CREATE" | "UPDATE" | "DELETE" }[]; + const Failures: { Regarding: string, Message: string }[] = []; + const Successes: { Regarding: string, Message: string }[] = []; + + for (const Entry of b) { + switch (Entry.Action) { + case "CREATE": + const Songs = await Promise.all(Entry.Songs.map(x => Song.findOne({ where: { ID: x } }))); + if (Songs.includes(null)) { + Failures.push({ Regarding: Entry.ID, Message: `Creation request for custom category "${Entry.Header}" tried to request a non-existing song.` }); + continue; + } + break; + + case "DELETE": + const DBEntry = await ForcedCategory.findOne({ where: { ID: Entry.ID } }); + if (!DBEntry) { + Failures.push({ Regarding: Entry.ID, Message: `Custom category "${Entry.ID}" doesn't exist.` }); + continue; + } + + await DBEntry.remove(); + Successes.push({ Regarding: Entry.ID, Message: `Successfully removed "${Entry.ID}" from the database.` }); + break; + + case "UPDATE": + break; + } + } + + res.status(Failures.length > Successes.length ? 400 : 200).json({ + Failures, + Successes + }) +}); + export default { App, DefaultAPI: "/admin/api" diff --git a/Server/Source/Routes/Authentication.ts b/Server/Source/Routes/Authentication.ts index c7573cd..f03b557 100644 --- a/Server/Source/Routes/Authentication.ts +++ b/Server/Source/Routes/Authentication.ts @@ -3,7 +3,7 @@ import jwt from "jsonwebtoken"; import qs from "querystring"; import { Response, Router } from "express"; import { DASHBOARD_ROOT, DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, FULL_SERVER_ROOT, JWT_KEY } from "../Modules/Constants"; -import { User } from "../Schemas/User"; +import { User, UserPermissions } from "../Schemas/User"; const App = Router(); @@ -36,14 +36,15 @@ App.get("/discord", async (req, res) => { await QuickRevokeToken(res, Discord.data.access_token); - if (!await User.exists({ where: { ID: UserData.data.id } })) - await User.create({ + let DBUser = await User.findOne({ where: { ID: UserData.data.id } }); + if (!DBUser) + DBUser = await User.create({ ID: UserData.data.id, Library: [] }).save(); const JWT = jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" }); - const UserDetails = Buffer.from(JSON.stringify({ ID: UserData.data.id, Username: UserData.data.username, GlobalName: UserData.data.global_name, Avatar: `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp` })).toString("hex") + const UserDetails = Buffer.from(JSON.stringify({ ID: UserData.data.id, Username: UserData.data.username, GlobalName: UserData.data.global_name, Avatar: `https://cdn.discordapp.com/avatars/${UserData.data.id}/${UserData.data.avatar}.webp`, IsAdmin: DBUser.PermissionLevel >= UserPermissions.Administrator })).toString("hex") if (req.query.state) { try { const Decoded = JSON.parse(Buffer.from(decodeURI(req.query.state as string), "base64").toString("utf-8")); diff --git a/Server/Source/Routes/Discovery.ts b/Server/Source/Routes/Discovery.ts index 6c2733b..817c089 100644 --- a/Server/Source/Routes/Discovery.ts +++ b/Server/Source/Routes/Discovery.ts @@ -9,13 +9,15 @@ App.get("/", async (req, res) => { const New = { ID: "new", Header: "Recently added", - Songs: (await Song.find({ take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()) + Songs: (await Song.find({ take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()), + Priority: 100, + Custom: false } res.json([ - ...ForcedCategories, + ...ForcedCategories.map(x => { return { ...x, Custom: true }; }), New - ]) + ].sort((a, b) => a.Priority - b.Priority)) }); export default { diff --git a/Server/Source/Routes/Library.ts b/Server/Source/Routes/Library.ts index 64031c2..46f6e77 100644 --- a/Server/Source/Routes/Library.ts +++ b/Server/Source/Routes/Library.ts @@ -75,7 +75,7 @@ ValidateBody(j.object({ async (req, res) => { const idx = req.user!.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()); if (idx === -1) - return res.status(400).json({ errorMessage: "You arent subscribed to this song." }); + return res.status(400).json({ errorMessage: "You aren't subscribed to this song." }); req.user?.BookmarkedSongs.splice(idx, 1); req.user?.save(); diff --git a/Server/Source/Schemas/User.ts b/Server/Source/Schemas/User.ts index 27fa05f..ac83370 100644 --- a/Server/Source/Schemas/User.ts +++ b/Server/Source/Schemas/User.ts @@ -2,6 +2,13 @@ import { BaseEntity, Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryCo import { Song } from "./Song"; import { Rating } from "./Rating"; +export enum UserPermissions { // increments of 100 in case we want to add permissions inbetween without fucking up all instances + User = 100, + VerifiedUser = 200, + Moderator = 300, + Administrator = 400 +} + @Entity() export class User extends BaseEntity { @PrimaryColumn() @@ -10,6 +17,9 @@ export class User extends BaseEntity { @Column({ type: "simple-json" }) Library: { SongID: string, Overriding: string }[]; + @Column({ default: UserPermissions.User }) + PermissionLevel: UserPermissions; + @OneToMany(() => Rating, R => R.Author) Ratings: Rating[]; diff --git a/Server/package-lock.json b/Server/package-lock.json index c02c86b..d1a0ea3 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@types/cors": "^2.8.17", + "@types/fluent-ffmpeg": "^2.1.24", "@types/uuid": "^9.0.7", "axios": "^1.6.5", "better-sqlite3": "^9.3.0", @@ -19,6 +20,7 @@ "dotenv": "^16.3.1", "exif-reader": "^2.0.0", "express": "^4.18.2", + "fluent-ffmpeg": "^2.1.2", "joi": "^17.12.0", "jsonwebtoken": "^9.0.2", "typeorm": "^0.3.19", @@ -156,6 +158,14 @@ "@types/send": "*" } }, + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", + "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", @@ -281,6 +291,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -968,6 +983,29 @@ "node": ">= 0.8" } }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", diff --git a/Server/package.json b/Server/package.json index d365bce..21f13e5 100644 --- a/Server/package.json +++ b/Server/package.json @@ -34,12 +34,13 @@ "@types/jsonwebtoken": "^9.0.5", "@types/node": "^20.6.3", "@types/underscore": "^1.11.15", + "@types/cors": "^2.8.17", + "@types/fluent-ffmpeg": "^2.1.24", + "@types/uuid": "^9.0.7", "tslib": "^2.6.2", "typescript": "^5.2.2" }, "dependencies": { - "@types/cors": "^2.8.17", - "@types/uuid": "^9.0.7", "axios": "^1.6.5", "better-sqlite3": "^9.3.0", "colorette": "^2.0.20", @@ -48,6 +49,7 @@ "dotenv": "^16.3.1", "exif-reader": "^2.0.0", "express": "^4.18.2", + "fluent-ffmpeg": "^2.1.2", "joi": "^17.12.0", "jsonwebtoken": "^9.0.2", "typeorm": "^0.3.19", diff --git a/src/App.tsx b/src/App.tsx index c0a277d..c27b88f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import "./utils/Requests"; import { useState } from "react"; import { ToastContainer } from "react-toastify"; import { BaseStyles, ThemeProvider, theme } from "@primer/react"; @@ -6,13 +7,14 @@ import { VerifyAdmin } from "./components/VerifyAdmin"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import { CookiesProvider } from "react-cookie"; import { Home } from "./routes/Home"; -import { AdminTrackList } from "./routes/AdminTrackList"; -import { AdminHome } from "./routes/AdminHome"; -import { AdminLogin } from "./routes/AdminLogin"; import { Download } from "./routes/Download"; import { Tracks } from "./routes/Tracks"; import { Profile } from "./routes/Profile"; +import { NotFound } from "./routes/404"; +import { AdminHome } from "./routes/AdminHome"; +import { AdminTrackList } from "./routes/AdminTrackList"; import { AdminCreateTrack } from "./routes/AdminCreateTrack"; +import { AdminFeaturedTab } from "./routes/AdminFeaturedTab"; import { SiteContext, SiteState } from "./utils/State"; import merge from "deepmerge"; @@ -40,12 +42,13 @@ function App() { } /> } /> } /> + } /> {/* Admin routes */} } /> - } /> {/* this is the only publically available admin endpoint */} } /> } /> + } /> diff --git a/src/components/AdminCategory.tsx b/src/components/AdminCategory.tsx new file mode 100644 index 0000000..c17b40a --- /dev/null +++ b/src/components/AdminCategory.tsx @@ -0,0 +1,40 @@ +import { Box, Heading, IconButton, Text } from "@primer/react"; +import { TrashIcon, PencilIcon, ChevronUpIcon, ChevronDownIcon } from "@primer/octicons-react" +import { Song } from "./Song"; + +export function AdminCategory({ categoryName, songs, isForced, moveUp, moveDown, onDelete, priority }: { categoryName: string, songs: any[], isForced: boolean, moveUp: () => void, moveDown: () => void, onDelete: () => void, priority: number }) { + return ( + + + {categoryName} + Priority: {priority}
+ { + isForced ? + You cannot edit the songs inside of this category, as it is forced. : + This is a custom, category managed by this instance's admins. + } + + { + !isForced ? + <> + + + + + : + <> + } + +
+ { + + } + + { + songs.map(x => ) + } + + +
+ ) +} \ No newline at end of file diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx index 77bb506..2e09d6f 100644 --- a/src/components/SiteHeader.tsx +++ b/src/components/SiteHeader.tsx @@ -2,12 +2,12 @@ import { Avatar, Box, Header } from "@primer/react"; import { SignInIcon } from "@primer/octicons-react" import { useCookies } from "react-cookie"; import { useNavigate } from "react-router-dom"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useState } from "react"; import { SiteContext, UserDetailInterface } from "../utils/State"; +import { toast } from "react-toastify"; import { Buffer } from "buffer/"; import Favicon from "../assets/favicon.webp"; import axios from "axios"; -import { toast } from "react-toastify"; export function SiteHeader() { const {state, setState} = useContext(SiteContext); @@ -40,7 +40,7 @@ export function SiteHeader() { navigate("/faq")}>FAQ window.open("https://discord.gg/KaxknAbqDS")}>Discord navigate("/download")}>Download - { cookies["AdminKey"] ? navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin : <> } + { state.UserDetails?.IsAdmin ? navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin : <> } { cookies["Token"] && state.UserDetails ? navigate("/profile")}> : diff --git a/src/components/Song.tsx b/src/components/Song.tsx index 0d27b6d..0698314 100644 --- a/src/components/Song.tsx +++ b/src/components/Song.tsx @@ -1,16 +1,18 @@ import { Box, Text } from "@primer/react"; import { Divider } from "@primer/react/lib-esm/ActionList/Divider"; -export function Song({ data, children }: { data: any, children: JSX.Element[] | JSX.Element | string }) { +export function Song({ data, children }: { data: any, children?: JSX.Element[] | JSX.Element | string }) { return (
{data.ArtistName} - {data.Name} - + {data.Name} + { + children ? : <> + }
- {children} + {children ?? <>}
) } \ No newline at end of file diff --git a/src/components/VerifyAdmin.tsx b/src/components/VerifyAdmin.tsx index b5a5487..089375c 100644 --- a/src/components/VerifyAdmin.tsx +++ b/src/components/VerifyAdmin.tsx @@ -1,11 +1,16 @@ -import { useCookies } from "react-cookie"; +import { useContext } from "react"; import { Navigate } from "react-router-dom"; +import { SiteContext } from "../utils/State"; +import { toast } from "react-toastify"; export function VerifyAdmin({ children }: { children: JSX.Element }) { - const [cookies] = useCookies(); + const {state} = useContext(SiteContext); - if (!cookies["AdminKey"]) - return ; + if (!state.UserDetails?.IsAdmin) + { + toast("Your account does not have admin permissions required to access this page. Try again later!", { type: "error" }); + return ; + } return children; } \ No newline at end of file diff --git a/src/css/index.css b/src/css/index.css index 85c72af..f7650dc 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -2,6 +2,7 @@ padding: 10px; margin-left: 15%; margin-right: 15%; + margin-bottom: 100px; } .songCategory { diff --git a/src/routes/404.tsx b/src/routes/404.tsx new file mode 100644 index 0000000..d98b1fc --- /dev/null +++ b/src/routes/404.tsx @@ -0,0 +1,13 @@ +import { Box, Heading, Link, Text } from "@primer/react"; + +export function NotFound() { + return ( + +
+ 404 + We're sorry, but this page does not exist.
+ Go back to the Home Page +
+
+ ) +} \ No newline at end of file diff --git a/src/routes/AdminFeaturedTab.tsx b/src/routes/AdminFeaturedTab.tsx new file mode 100644 index 0000000..f0a6758 --- /dev/null +++ b/src/routes/AdminFeaturedTab.tsx @@ -0,0 +1,86 @@ +import { Box, Button, Dialog, Heading } from "@primer/react"; +import { AdminCategory } from "../components/AdminCategory"; +import { useEffect, useState } from "react"; +import { moveElement } from "../utils/Extensions"; +import { toast } from "react-toastify"; +import axios from "axios"; + +export function AdminFeaturedTab() { + const [library, setLibrary] = useState<{ ID: string, Header: string, Songs: unknown[], Custom: boolean, Priority: number }[] | null>(null); + const [hackyRevertChanges, setHackyRevertChanges] = useState(false); + + useEffect(() => { + (async () => { + const Featured = await axios.get("/api/discovery"); + if (Featured.status !== 200) + return toast("Something went wrong while loading discovery. Try again later.", { type: "error" }); + + setLibrary(Featured.data); + })(); + }, [hackyRevertChanges]); + + return ( + <> + + Featured Tabs + { + library?.map((x, i) => { + return ( + { + if (i + 1 >= library.length) + return toast("You cannot move this category further down.", { type: "error" }); + + const Sorted = library.sort((a, b) => a.Priority - b.Priority); + + const Idx = Sorted.findIndex(y => y.ID === x.ID); + + console.log(Sorted); + moveElement(Idx, Idx + 1, Sorted); + console.log(Sorted); + + setLibrary(Sorted.map((y, idx, a) => { return { + ...y, + Priority: + y.Custom ? + (idx === 0 ? 0 : a[idx - 1].Priority + 1) : + y.Priority + }; })); + }} + moveUp={() => { + if (i - 1 < 0) + return toast("You cannot move this category further up.", { type: "error" }); + + const Sorted = library.sort((a, b) => a.Priority - b.Priority); + + const Idx = Sorted.findIndex(y => y.ID === x.ID); + + console.log(Sorted); + moveElement(Idx, Idx - 1, Sorted); + console.log(Sorted); + + setLibrary(Sorted.map((y, idx, a) => { return { + ...y, + Priority: + y.Custom ? + (idx === 0 ? 0 : a[idx - 1].Priority + 1) : + y.Priority + }; }).sort((a, b) => a.Priority - b.Priority)); + }} + onDelete={() => { + + }} /> + ) + }) + } + + + + + + ) +} \ No newline at end of file diff --git a/src/routes/AdminHome.tsx b/src/routes/AdminHome.tsx index 317d2be..a33bd56 100644 --- a/src/routes/AdminHome.tsx +++ b/src/routes/AdminHome.tsx @@ -1,19 +1,10 @@ import { Box, Button, Text } from "@primer/react"; import { PageHeader } from "@primer/react/drafts"; -import { useEffect, useState } from "react"; -import { VerifyAdminKey } from "../utils/AdminUtil"; -import { useCookies } from "react-cookie"; import { useNavigate } from "react-router-dom"; export function AdminHome() { - const [keyValid, setKeyValid] = useState(false); - const [cookies, , removeCookie] = useCookies(); // how in the fuck is this valid ts syntax??????????? const navigate = useNavigate(); - useEffect(() => { - (async() => setKeyValid((await VerifyAdminKey(cookies["AdminKey"])).Success))(); - }, [cookies]); - return ( @@ -21,8 +12,9 @@ export function AdminHome() { Partypack Admin Management Panel - Your admin key is { keyValid ? VALID : INVALID } - + TEMP + + diff --git a/src/routes/AdminLogin.tsx b/src/routes/AdminLogin.tsx deleted file mode 100644 index bbf8e10..0000000 --- a/src/routes/AdminLogin.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Box, Button, Text, TextInput } from "@primer/react"; -import { useEffect, useRef, useState } from "react"; -import { VerifyAdminKey } from "../utils/AdminUtil"; -import { useCookies } from "react-cookie"; -import { useNavigate } from "react-router-dom"; - -export function AdminLogin() { - const [errorMessage, setErrorMessage] = useState(""); - const [success, setSuccess] = useState(false); - const [cookies, setCookie, removeCookie] = useCookies(); - const navigate = useNavigate(); - const KeyInputRef = useRef(null); - - useEffect(() => { - if (cookies["AdminKey"]) - navigate("/admin"); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - -
- Provide the top secret admin key defined in the environment below:
- - - { - errorMessage !== "" ? ( - {errorMessage} - ) : <> - } -
-
- ) -} \ No newline at end of file diff --git a/src/routes/Profile.tsx b/src/routes/Profile.tsx index d95f7c6..1733307 100644 --- a/src/routes/Profile.tsx +++ b/src/routes/Profile.tsx @@ -49,7 +49,7 @@ export function Profile() { {state.UserDetails.GlobalName} (@{state.UserDetails.Username}) - + diff --git a/src/utils/Extensions.ts b/src/utils/Extensions.ts new file mode 100644 index 0000000..03155f5 --- /dev/null +++ b/src/utils/Extensions.ts @@ -0,0 +1,3 @@ +export function moveElement(fromIndex: number, toIndex: number, array: unknown[]) { + array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]); +} \ No newline at end of file diff --git a/src/utils/State.ts b/src/utils/State.ts index b0f9f0d..171ef30 100644 --- a/src/utils/State.ts +++ b/src/utils/State.ts @@ -3,7 +3,7 @@ import { createContext } from "react"; export const SiteContext = createContext({} as IContext); -export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string } +export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string, IsAdmin: boolean } export interface SiteState { UserDetails: UserDetailInterface | null;