commit via mc's programming deck
This commit is contained in:
parent
bf72a67083
commit
a0cc212aa5
|
@ -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)") : "")}`));
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
38
Server/package-lock.json
generated
38
Server/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
11
src/App.tsx
11
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() {
|
|||
<Route path="/download" element={<Download />} />
|
||||
<Route path="/tracks" element={<Tracks />} />
|
||||
<Route path="/profile" element={<Profile />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
|
||||
{/* Admin routes */}
|
||||
<Route path="/admin" element={<VerifyAdmin><AdminHome /></VerifyAdmin>} />
|
||||
<Route path="/admin/login" element={<AdminLogin />} /> {/* this is the only publically available admin endpoint */}
|
||||
<Route path="/admin/tracks" element={<VerifyAdmin><AdminTrackList /></VerifyAdmin>} />
|
||||
<Route path="/admin/tracks/create" element={<VerifyAdmin><AdminCreateTrack /></VerifyAdmin>} />
|
||||
<Route path="/admin/featured" element={<VerifyAdmin><AdminFeaturedTab /></VerifyAdmin>} />
|
||||
</Routes>
|
||||
</div>
|
||||
</SiteContext.Provider>
|
||||
|
|
40
src/components/AdminCategory.tsx
Normal file
40
src/components/AdminCategory.tsx
Normal file
|
@ -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 (
|
||||
<Box m={2} sx={{ overflow: "hidden", width: "100%", padding: 3, borderRadius: 10, border: "solid", borderColor: "border.default" }}>
|
||||
<Box>
|
||||
<Heading>{categoryName}</Heading>
|
||||
<Text>Priority: <b>{priority}</b></Text><br />
|
||||
{
|
||||
isForced ?
|
||||
<Text>You cannot edit the songs inside of this category, as it is forced.</Text> :
|
||||
<Text>This is a custom, category managed by this instance's admins.</Text>
|
||||
}
|
||||
<Box sx={{ display: "inline-flex", gap: 2, float: "right" }}>
|
||||
{
|
||||
!isForced ?
|
||||
<>
|
||||
<IconButton icon={PencilIcon} variant="primary" aria-label="Default" />
|
||||
<IconButton icon={TrashIcon} variant="danger" aria-label="Default" onClick={onDelete} />
|
||||
<IconButton icon={ChevronUpIcon} aria-label="Default" onClick={moveUp} />
|
||||
<IconButton icon={ChevronDownIcon} aria-label="Default" onClick={moveDown} />
|
||||
</> :
|
||||
<></>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
{
|
||||
|
||||
}
|
||||
<Box p={1} className="songCategory">
|
||||
{
|
||||
songs.map(x => <Song data={x} />)
|
||||
}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -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() {
|
|||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/faq")}>FAQ</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => window.open("https://discord.gg/KaxknAbqDS")}>Discord</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer", color: "accent.emphasis" }} onClick={() => navigate("/download")}>Download</Header.Item>
|
||||
{ cookies["AdminKey"] ? <Header.Item full onClick={() => navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin</Header.Item> : <></> }
|
||||
{ state.UserDetails?.IsAdmin ? <Header.Item full onClick={() => navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin</Header.Item> : <></> }
|
||||
{
|
||||
cookies["Token"] && state.UserDetails ?
|
||||
<Header.Item sx={{ mr: 0, cursor: "pointer" }} onClick={() => navigate("/profile")}><Avatar src={state.UserDetails.Avatar} size={25} alt={`${state.UserDetails.GlobalName} (@${state.UserDetails.Username})`}/></Header.Item> :
|
||||
|
|
|
@ -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 (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 50, maxWidth: 200, padding: 2, borderRadius: 10, border: "solid", borderColor: "border.default" }}>
|
||||
<img src={data.Cover} style={{ width: "100%", borderRadius: 10 }} />
|
||||
<center>
|
||||
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{data.ArtistName}</Text>
|
||||
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{data.Name}</Text>
|
||||
<Divider />
|
||||
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}><b>{data.Name}</b></Text>
|
||||
{
|
||||
children ? <Divider /> : <></>
|
||||
}
|
||||
</center>
|
||||
{children}
|
||||
{children ?? <></>}
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -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 <Navigate to="/admin/login" replace />;
|
||||
if (!state.UserDetails?.IsAdmin)
|
||||
{
|
||||
toast("Your account does not have admin permissions required to access this page. Try again later!", { type: "error" });
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
padding: 10px;
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.songCategory {
|
||||
|
|
13
src/routes/404.tsx
Normal file
13
src/routes/404.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Box, Heading, Link, Text } from "@primer/react";
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<Box>
|
||||
<center>
|
||||
<Heading>404</Heading>
|
||||
<Text>We're sorry, but this page does not exist.</Text><br />
|
||||
<Link href="/">Go back to the Home Page</Link>
|
||||
</center>
|
||||
</Box>
|
||||
)
|
||||
}
|
86
src/routes/AdminFeaturedTab.tsx
Normal file
86
src/routes/AdminFeaturedTab.tsx
Normal file
|
@ -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<boolean>(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 (
|
||||
<>
|
||||
<Dialog></Dialog>
|
||||
<Heading>Featured Tabs</Heading>
|
||||
{
|
||||
library?.map((x, i) => {
|
||||
return (
|
||||
<AdminCategory
|
||||
priority={x.Priority}
|
||||
isForced={!x.Custom}
|
||||
categoryName={x.Header}
|
||||
songs={x.Songs}
|
||||
moveDown={() => {
|
||||
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={() => {
|
||||
|
||||
}} />
|
||||
)
|
||||
})
|
||||
}
|
||||
<Box sx={{ float: "right", display: "inline-flex", gap: 2 }}>
|
||||
<Button variant="primary">Save</Button>
|
||||
<Button onClick={() => setHackyRevertChanges(!hackyRevertChanges)}>Revert changes</Button>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
<Box>
|
||||
<PageHeader>
|
||||
|
@ -21,8 +12,9 @@ export function AdminHome() {
|
|||
<PageHeader.Title>Partypack Admin Management Panel</PageHeader.Title>
|
||||
</PageHeader.TitleArea>
|
||||
<PageHeader.Description>
|
||||
Your admin key is { keyValid ? <Text sx={{ color: "accent.emphasis" }}>VALID</Text> : <Text sx={{ color: "danger.emphasis" }}>INVALID</Text> }
|
||||
<Button variant="danger" size="small" onClick={() => {removeCookie("AdminKey"); navigate("/admin/login")}}>Log out</Button>
|
||||
TEMP
|
||||
<Button onClick={() => navigate("/admin/tracks")}>Tracks</Button>
|
||||
<Button onClick={() => navigate("/admin/featured")}>Featured Tab Management</Button>
|
||||
</PageHeader.Description>
|
||||
</PageHeader>
|
||||
</Box>
|
||||
|
|
|
@ -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<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (cookies["AdminKey"])
|
||||
navigate("/admin");
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<center>
|
||||
<Text>Provide the top secret admin key defined in the environment below:</Text><br />
|
||||
<TextInput ref={KeyInputRef} size="large" sx={{ minWidth: 400, maxWidth: 600 }} monospace={true} validationStatus={success ? "success" : "error"} />
|
||||
<Button variant="primary" onClick={
|
||||
async () => {
|
||||
const Key = KeyInputRef.current!.value;
|
||||
const Result = await VerifyAdminKey(Key);
|
||||
setSuccess(Result.Success);
|
||||
setErrorMessage(Result.Message);
|
||||
|
||||
const D = new Date();
|
||||
D.setUTCHours(D.getUTCHours() + 4);
|
||||
if (Result.Success)
|
||||
{
|
||||
setCookie("AdminKey", Key, { expires: D });
|
||||
navigate("/admin");
|
||||
return;
|
||||
}
|
||||
|
||||
removeCookie("AdminKey");
|
||||
}
|
||||
}>Log in</Button>
|
||||
{
|
||||
errorMessage !== "" ? (
|
||||
<Text sx={{ color: success ? "primary.emphasis" : "danger.emphasis" }}>{errorMessage}</Text>
|
||||
) : <></>
|
||||
}
|
||||
</center>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -49,7 +49,7 @@ export function Profile() {
|
|||
{state.UserDetails.GlobalName} (@{state.UserDetails.Username})
|
||||
</PageHeader.Title>
|
||||
<PageHeader.Actions>
|
||||
<Button size="large" variant="danger" onClick={() => { removeCookie("UserDetails"); removeCookie("Token"); setState({ ...state, UserDetails: null }); navigate("/"); }}>Log out</Button>
|
||||
<Button size="large" variant="danger" onClick={() => { removeCookie("UserDetails"); removeCookie("Token"); setState({ ...state, UserDetails: null }); window.location.assign("/") }}>Log out</Button>
|
||||
</PageHeader.Actions>
|
||||
</PageHeader.TitleArea>
|
||||
</PageHeader>
|
||||
|
|
3
src/utils/Extensions.ts
Normal file
3
src/utils/Extensions.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function moveElement(fromIndex: number, toIndex: number, array: unknown[]) {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||
}
|
|
@ -3,7 +3,7 @@ import { createContext } from "react";
|
|||
|
||||
export const SiteContext = createContext<IContext>({} 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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user