From a0cc212aa5bd001e99d8e301c481b9a4d841acc3 Mon Sep 17 00:00:00 2001
From: McMistrzYT <56406996+McMistrzYT@users.noreply.github.com>
Date: Fri, 26 Jan 2024 23:29:46 +0100
Subject: [PATCH] commit via mc's programming deck
---
Server/Source/Handlers/Server.ts | 5 ++
Server/Source/Routes/Admin.ts | 78 ++++++++++++++++++++---
Server/Source/Routes/Authentication.ts | 9 +--
Server/Source/Routes/Discovery.ts | 8 ++-
Server/Source/Routes/Library.ts | 2 +-
Server/Source/Schemas/User.ts | 10 +++
Server/package-lock.json | 38 ++++++++++++
Server/package.json | 6 +-
src/App.tsx | 11 ++--
src/components/AdminCategory.tsx | 40 ++++++++++++
src/components/SiteHeader.tsx | 6 +-
src/components/Song.tsx | 10 +--
src/components/VerifyAdmin.tsx | 13 ++--
src/css/index.css | 1 +
src/routes/404.tsx | 13 ++++
src/routes/AdminFeaturedTab.tsx | 86 ++++++++++++++++++++++++++
src/routes/AdminHome.tsx | 14 +----
src/routes/AdminLogin.tsx | 52 ----------------
src/routes/Profile.tsx | 2 +-
src/utils/Extensions.ts | 3 +
src/utils/State.ts | 2 +-
21 files changed, 311 insertions(+), 98 deletions(-)
create mode 100644 src/components/AdminCategory.tsx
create mode 100644 src/routes/404.tsx
create mode 100644 src/routes/AdminFeaturedTab.tsx
delete mode 100644 src/routes/AdminLogin.tsx
create mode 100644 src/utils/Extensions.ts
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;