commit via mc's programming deck

This commit is contained in:
McMistrzYT 2024-01-26 23:29:46 +01:00
parent bf72a67083
commit a0cc212aa5
21 changed files with 311 additions and 98 deletions

View File

@ -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)") : "")}`));
}

View File

@ -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"

View File

@ -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"));

View File

@ -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 {

View File

@ -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();

View File

@ -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[];

View File

@ -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",

View File

@ -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",

View File

@ -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>

View 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>
)
}

View File

@ -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> :

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -2,6 +2,7 @@
padding: 10px;
margin-left: 15%;
margin-right: 15%;
margin-bottom: 100px;
}
.songCategory {

13
src/routes/404.tsx Normal file
View 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>
)
}

View 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>
</>
)
}

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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
View File

@ -0,0 +1,3 @@
export function moveElement(fromIndex: number, toIndex: number, array: unknown[]) {
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
}

View File

@ -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;