actual backend implementation lol

This commit is contained in:
McMistrzYT 2024-01-22 00:41:59 +01:00
parent df13e78af6
commit 8d4436a123
29 changed files with 2136 additions and 83 deletions

Binary file not shown.

View File

@ -5,4 +5,8 @@ SERVER_URL=localhost
DASHBOARD_URL=localhost:5173 # set this to wherever you're hosting the Partypack dashboard
ENVIRONMENT=dev
COOKIE_SIGN_KEY=
ADMIN_KEY=
BOT_TOKEN=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
USE_HTTPS=true # set this to false if you're debugging on your computer

4
Server/.gitignore vendored
View File

@ -130,3 +130,7 @@ dist
.pnp.*
bin/
*.db
*.db-shm
*.db-wal

View File

@ -0,0 +1,36 @@
import { DataSource } from "typeorm";
import { ENVIRONMENT } from "../Modules/Constants";
import { join } from "path";
export const DBSource = new DataSource({
type: "better-sqlite3",
database: `Partypack${ENVIRONMENT !== "prod" ? `-${ENVIRONMENT}` : ""}.db`,
synchronize: true,
logging: false,
entities: [join(__dirname, "..", "Schemas") + "\\*{.js,.ts}"],
subscribers: [],
migrations: [],
enableWAL: true
});
(async () => {
await DBSource.initialize();
})();
/*
User
- discord id (primary)
- list of all songs in user's library
- list of all songs in user's published
Song
- length
- bpm
- key
- scale
- keytar/guitar
- icon url
- name
- artist
- release year
*/

View File

@ -51,6 +51,8 @@ Initialize();
// set up both utils for usage
import { LoadSongs } from "../Modules/FestivalUtil";
import { CacheFortnitePages } from "../Modules/PagesUtil";
import axios from "axios";
axios.defaults.validateStatus = () => true;
LoadSongs();
CacheFortnitePages();

View File

@ -0,0 +1,18 @@
// massive shoutout to yls for providing me with the blurl standard doc :fire:
import { deflateSync, inflateSync } from "zlib";
export function CreateBlurl(Data: unknown) { // mildly unsafe
const Buf = Buffer.from(JSON.stringify(Data));
const DefBuf = deflateSync(Buf);
const Length = Buffer.alloc(4);
Length.writeIntBE(Buf.toString().length, 0, 4)
return Buffer.concat([Buffer.from("blul"), Length, DefBuf]);
}
export function DeBlurl(Data: Buffer) { // i have absolutely ZERO clue whether this works so do not try it
const Buf = inflateSync(Data.subarray(4));
return Buf;
}

View File

@ -1,3 +1,6 @@
export const ENVIRONMENT = process.env.ENVIRONMENT ?? "dev";
export const IS_DEBUG = ENVIRONMENT.toLowerCase() === "dev" || ENVIRONMENT.toLowerCase() === "stage"; // IS_DEBUG can be used to enable test endpoints, unsafe code and more.
export const PROJECT_NAME = process.env.PROJECT_NAME ?? "BasedServer"; // Default prefix for the logger module.
export const BODY_SIZE_LIMIT = process.env.BODY_SIZE_LIMIT ?? "10mb"; // Doesn't accept requests with body sizes larger than this value.
export const SERVER_URL = process.env.SERVER_URL ?? "localhost"; // The server's URL. Not used for a lot by default.
@ -7,11 +10,12 @@ export const ENDPOINT_AUTHENTICATION_ENABLED = !!process.env.ENDPOINT_AUTHENTICA
export const _ENDPOINT_AUTHENTICATION_ENV = process.env.ENDPOINT_AUTHENTICATION;
export const ENDPOINT_AUTH_HEADER = _ENDPOINT_AUTHENTICATION_ENV?.split(":")[0]; // Header name for endpoint auth.
export const ENDPOINT_AUTH_VALUE = _ENDPOINT_AUTHENTICATION_ENV?.split(":")[1]; // Value of the header for endpoint auth.
export const USE_HTTPS = false;//Boolean(process.env.USE_HTTPS); // todo: fix this shit
export const USE_HTTPS = !IS_DEBUG; //false; //Boolean(process.env.USE_HTTPS); // todo: fix this shit
export const DASHBOARD_ROOT = `http${USE_HTTPS ? "s" : ""}://${DASHBOARD_URL}`; // A shortcut so that you don't need to type this out every time you wanna display the dashboard URL.
export const FULL_SERVER_ROOT = `http${USE_HTTPS ? "s" : ""}://${SERVER_URL}:${PORT}`; // A shortcut so that you don't need to type this out every time you wanna display the server URL.
export const COOKIE_SIGN_KEY = process.env.COOKIE_SIGN_KEY; // Secret that will be used to sign cookies.
export const ADMIN_KEY = process.env.ADMIN_KEY; // Secret that will be required to sign into the Admin Dashboard.
export const ENVIRONMENT = process.env.ENVIRONMENT ?? "dev";
export const IS_DEBUG = ENVIRONMENT.toLowerCase() === "dev" || ENVIRONMENT.toLowerCase() === "stage"; // IS_DEBUG can be used to enable test endpoints, unsafe code and more.
export const JWT_KEY = process.env.JWT_KEY; // Secret that will be required to sign JSON Web Tokens (JWTs).
export const BOT_TOKEN = process.env.BOT_TOKEN; // Used for Discord-related stuff.
export const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; // Client ID for authentication and checking what role you have on the Discord server.
export const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; // Client secret for authentication and checking what role you have on the Discord server.

View File

@ -0,0 +1,47 @@
import axios from "axios";
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";
const App = Router();
// ? hacky, if you want, make it less hacky
async function QuickRevokeToken(res: Response, Token: string) {
await axios.post("https://discord.com/api/oauth2/token/revoke", qs.stringify({ token: Token, token_type_hint: "access_token" }), { auth: { username: DISCORD_CLIENT_ID!, password: DISCORD_CLIENT_SECRET! } })
return res;
}
App.get("/discord/url", (_ ,res) => res.send(`https://discord.com/api/oauth2/authorize?client_id=${qs.escape(DISCORD_CLIENT_ID!)}&response_type=code&redirect_uri=${qs.escape(`${FULL_SERVER_ROOT}/api/discord`)}&scope=identify`))
App.get("/discord", async (req, res) => {
if (!req.query.code)
return res.status(400).send("Please authorize with Discord first.");
if (!/^(\w|\d)+$/gi.test(req.query.code as string))
return res.status(400).send("Malformed code.");
const Discord = await axios.post(`https://discord.com/api/oauth2/token`, qs.stringify({ grant_type: "authorization_code", code: req.query.code as string, redirect_uri: `${FULL_SERVER_ROOT}/api/discord` }), { auth: { username: DISCORD_CLIENT_ID!, password: DISCORD_CLIENT_SECRET! } });
if (Discord.status !== 200)
return res.status(500).send("Failed to request OAuth token from Discord's services.");
if (!Discord.data.scope.includes("identify"))
return (await QuickRevokeToken(res, Discord.data.access_token)).status(400).send("Missing identify scope. Please check if your OAuth link is correctly set up!");
const UserData = await axios.get(`https://discord.com/api/users/@me`, { headers: { Authorization: `${Discord.data.token_type} ${Discord.data.access_token}` } });
if (UserData.status !== 200)
return (await QuickRevokeToken(res, Discord.data.access_token)).status(500).send("Failed to request user data from Discord's services.");
await QuickRevokeToken(res, Discord.data.access_token);
res
.cookie("Token", jwt.sign({ ID: UserData.data.id }, JWT_KEY!, { algorithm: "HS256" }))
.cookie("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("base64"))
.redirect(`${DASHBOARD_ROOT}/profile`);
})
export default {
App,
DefaultAPI: "/api"
}

View File

@ -0,0 +1,67 @@
import { Router } from "express";
import { existsSync, readFileSync } from "fs";
import { AvailableFestivalSongs } from "../Modules/FestivalUtil";
import { FULL_SERVER_ROOT } from "../Modules/Constants";
import { CreateBlurl } from "../Modules/BLURL";
const App = Router();
App.get("/song/download/:SongUUID/:File", (req, res) => {
const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID);
if (!Song)
return res.sendStatus(404);
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${Song.UUID}/`;
switch (req.params.File.toLowerCase()) {
case "master.blurl":
case "main.blurl":
return res.set("content-type", "text/plain").send(
CreateBlurl({
playlists: [
{
type: "main",
language: "en",
url: `${BaseURL}master.blurl`,
data: readFileSync(`${Song.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL)
}
],
type: "vod",
audioonly: true
})
);
case "manifest":
case "manifest.mpd":
return res.set("content-type", "application/dash+xml").send(Buffer.from(readFileSync(`${Song.Directory}/Manifest.mpd`).toString().replaceAll("{BASEURL}", BaseURL)));
case "cover":
case "cover.png":
return existsSync(`${Song.Directory}/Cover.png`) ? res.set("content-type", "image/png").send(readFileSync(`${Song.Directory}/Cover.png`)) : res.sendStatus(404);
// ! we are not risking a lawsuit
//case "midi.dat": // dont forget to encrypt!
//return existsSync(`${Song.Directory}/Data.mid`) ? res.set("content-type", "application/octet-stream").send(AesEncrypt(readFileSync(`${Song.Directory}/Data.mid`))) : res.sendStatus(404);
// funny little tip: you dont actually need to encrypt midis LMFAO
case "midi":
case "midi.mid":
case "midi.midi": // forget to encrypt!
return existsSync(`${Song.Directory}/Data.mid`) ? res.set("content-type", "application/octet-stream").send(readFileSync(`${Song.Directory}/Data.mid`)) : res.sendStatus(404);
}
if (!/^[\w\-.]+$/g.test(req.params.File))
return res.status(400).send("File name failed validation.");
if (!req.params.File.endsWith(".m4s"))
return res.sendStatus(403);
if (!existsSync(`${Song.Directory}/Chunks/${req.params.File}`))
return res.sendStatus(404);
res.set("content-type", "video/mp4")
res.send(readFileSync(`${Song.Directory}/Chunks/${req.params.File}`));
})
export default {
App
}

View File

@ -1,11 +0,0 @@
import { Router } from "express";
const App = Router();
App.get("/", (_, res) => res.send("This content should be served on <b>/welcome</b>!<br><a href=\"/welcome/sub\">Go to sub-page</a>"))
App.get("/sub", (_, res) => res.send("Welcome to the sub-page! This content should be served on <b>/welcome/sub</b>!<br><a href=\"/welcome\">Go back</a>"))
export default {
App,
DefaultAPI: "/welcome"
}

View File

@ -0,0 +1,14 @@
import { Router } from "express";
const App = Router();
App.get("/song/data/:InternalID", (req, res) => {
res.json({
})
})
export default {
App,
DefaultAPI: "/api/library"
}

View File

@ -1,9 +0,0 @@
import { Router } from "express";
const App = Router();
App.get("/", (_, res) => res.send("Welcome to the root page. <br><a href=\"/welcome\">Log in</a>"))
export default {
App
}

View File

@ -0,0 +1,40 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Song extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
ID: string;
@Column()
Name: string;
@Column()
Year: number;
@Column()
ArtistName: string;
@Column()
Length: number;
@Column()
Scale: "Minor" | "Major";
@Column()
Key: string;
@Column()
Album: string;
@Column({ default: "Guitar" })
GuitarStarterType: "Keytar" | "Guitar";
@Column()
Tempo: number;
@Column()
Cover: string;
@Column({ nullable: true })
Lipsync?: string;
}

View File

View File

@ -1,6 +1,7 @@
import { config } from "dotenv";
config();
import "./Handlers/Database";
import "./Handlers/Server";
/*

1522
Server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@
"devDependencies": {
"@types/cookie-parser": "^1.4.6",
"@types/express": "^4.17.18",
"@types/jsonwebtoken": "^9.0.5",
"@types/node": "^20.6.3",
"tslib": "^2.6.2",
"typescript": "^5.2.2"
@ -38,11 +39,14 @@
"dependencies": {
"@types/cors": "^2.8.17",
"axios": "^1.6.5",
"better-sqlite3": "^9.3.0",
"colorette": "^2.0.20",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2"
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"typeorm": "^0.3.19"
},
"homepage": "https://github.com/McMistrzYT/BasedServer#readme"
}

194
package-lock.json generated
View File

@ -8,15 +8,20 @@
"name": "snippets",
"version": "0.0.0",
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@primer/react": "^36.5.0",
"axios": "^1.6.5",
"buffer": "^6.0.3",
"deepmerge": "^4.3.1",
"node-watch": "^0.7.4",
"querystring-es3": "^0.2.1",
"react": "^18.2.0",
"react-cookie": "^7.0.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"react-toastify": "^9.1.3",
"rollup-plugin-node-polyfills": "^0.2.1",
"styled-components": "^5.3.11"
},
"devDependencies": {
@ -434,6 +439,37 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
},
"node_modules/@esbuild-plugins/node-globals-polyfill": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
"integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==",
"peerDependencies": {
"esbuild": "*"
}
},
"node_modules/@esbuild-plugins/node-modules-polyfill": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz",
"integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==",
"dependencies": {
"escape-string-regexp": "^4.0.0",
"rollup-plugin-node-polyfills": "^0.2.1"
},
"peerDependencies": {
"esbuild": "*"
}
},
"node_modules/@esbuild-plugins/node-modules-polyfill/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
@ -441,7 +477,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
@ -457,7 +492,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -473,7 +507,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -489,7 +522,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -505,7 +537,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -521,7 +552,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -537,7 +567,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -553,7 +582,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -569,7 +597,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -585,7 +612,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -601,7 +627,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -617,7 +642,6 @@
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -633,7 +657,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -649,7 +672,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -665,7 +687,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -681,7 +702,6 @@
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -697,7 +717,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -713,7 +732,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
@ -729,7 +747,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
@ -745,7 +762,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
@ -761,7 +777,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -777,7 +792,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -793,7 +807,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -1545,6 +1558,17 @@
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
"integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
@ -1959,6 +1983,25 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -2020,6 +2063,29 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2318,7 +2384,6 @@
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
"integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@ -2641,6 +2706,11 @@
"node": ">=4.0"
}
},
"node_modules/estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@ -2966,6 +3036,25 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@ -3259,6 +3348,14 @@
"yallist": "^3.0.2"
}
},
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
"dependencies": {
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/mdast-util-definitions": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz",
@ -4071,6 +4168,14 @@
"node": ">=6"
}
},
"node_modules/querystring-es3": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -4330,6 +4435,33 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-inject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
"integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==",
"deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.",
"dependencies": {
"estree-walker": "^0.6.1",
"magic-string": "^0.25.3",
"rollup-pluginutils": "^2.8.1"
}
},
"node_modules/rollup-plugin-node-polyfills": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz",
"integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==",
"dependencies": {
"rollup-plugin-inject": "^3.0.0"
}
},
"node_modules/rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dependencies": {
"estree-walker": "^0.6.1"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -4466,6 +4598,12 @@
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead"
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
@ -4665,6 +4803,14 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",

View File

@ -12,15 +12,20 @@
"preview": "vite preview"
},
"dependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@primer/react": "^36.5.0",
"axios": "^1.6.5",
"buffer": "^6.0.3",
"deepmerge": "^4.3.1",
"node-watch": "^0.7.4",
"querystring-es3": "^0.2.1",
"react": "^18.2.0",
"react-cookie": "^7.0.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3",
"react-toastify": "^9.1.3",
"rollup-plugin-node-polyfills": "^0.2.1",
"styled-components": "^5.3.11"
},
"devDependencies": {

View File

@ -1,21 +1,27 @@
import { ToastContainer } from "react-toastify";
import { BaseStyles, ThemeProvider, theme } from "@primer/react";
import { SiteHeader } from "./components/SiteHeader";
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 { VerifyAdmin } from "./components/VerifyAdmin";
import { Download } from "./routes/Download";
import { Profile } from "./routes/Profile";
import { SiteContext, SiteState } from "./utils/State";
import merge from "deepmerge";
import "react-toastify/dist/ReactToastify.css";
import "./css/index.css";
import { useState } from "react";
const DefaultTheme = merge(theme, {}); // we'll use this!! eventually!!!
function App() {
const [reactState, setReactState] = useState<SiteState>({} as SiteState);
return (
<ThemeProvider colorMode="dark" theme={DefaultTheme}>
<BaseStyles>
@ -23,18 +29,22 @@ function App() {
<CookiesProvider />
<ToastContainer theme="dark" position="bottom-right" draggable={false} pauseOnHover={false} pauseOnFocusLoss={false} />
<BrowserRouter>
<SiteContext.Provider value={{ state: reactState, setState: setReactState }}>
<SiteHeader />
<div className="content">
<Routes>
{/* User-accessible routes */}
<Route path="/" element={<Home />} />
<Route path="/download" element={<Download />} />
<Route path="/profile" element={<Profile />} />
{/* Admin routes */}
<Route path="/admin/login" element={<AdminLogin />} />
<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>} />
</Routes>
</div>
</SiteContext.Provider>
</BrowserRouter>
</div>
</BaseStyles>

View File

@ -1,26 +1,59 @@
import { Header } from "@primer/react";
import { Avatar, Box, Header } from "@primer/react";
import { SignInIcon } from "@primer/octicons-react"
import { useCookies } from "react-cookie";
import Favicon from "../assets/favicon.webp";
import { useNavigate } from "react-router-dom";
import { useContext, useEffect, useState } from "react";
import { SiteContext, UserDetailInterface } from "../utils/State";
import { Buffer } from "buffer/";
import Favicon from "../assets/favicon.webp";
import axios from "axios";
export function SiteHeader() {
const [discordUrl, setDiscordUrl] = useState("");
const {state, setState} = useContext(SiteContext);
const [cookies] = useCookies();
const navigate = useNavigate();
useEffect(() => {
(async () => {
const Data = await axios.get("/api/discord/url");
if (Data.status === 200)
setDiscordUrl(Data.data);
})();
}, []);
useEffect(() => {
if (!cookies["UserDetails"])
return;
const Details: UserDetailInterface = JSON.parse(decodeURI(Buffer.from(cookies["UserDetails"], "base64").toString()));
setState({
...state,
UserDetails: Details
});
console.log(Details);
}, [cookies["UserDetails"]])
return (
<Header>
<Header.Item sx={{ cursor: "pointer" }} onClick={() => navigate("/")}>
<img src={Favicon} style={{ width: 32, height: "auto", paddingRight: 5 }} />
<b>Partypack</b>
</Header.Item>
<Header.Item sx={{ cursor: "pointer" }}>Daily Rotation</Header.Item>
<Header.Item sx={{ cursor: "pointer" }}>Leaderboards</Header.Item>
<Header.Item sx={{ cursor: "pointer" }}>Tracks</Header.Item>
<Header.Item sx={{ cursor: "pointer" }}>Tutorials</Header.Item>
<Header.Item sx={{ cursor: "pointer" }}>FAQ</Header.Item>
<Header.Item sx={{ cursor: "pointer" }} onClick={() => window.open("https://discord.gg/KaxknAbqDS")}>Discord</Header.Item>
<Header.Item sx={{ cursor: "pointer", color: "accent.emphasis" }}>Download</Header.Item>
{ cookies["AdminKey"] ? <Header.Item onClick={() => navigate("/admin")} sx={{ cursor: "pointer", color: "danger.emphasis" }}>Admin</Header.Item> : <></> }
<Header.Item full sx={{ cursor: "pointer" }}>Daily Rotation</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }}>Leaderboards</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }}>Tracks</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }}>Tutorials</Header.Item>
<Header.Item full sx={{ cursor: "pointer" }}>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" }}>Download</Header.Item>
{ cookies["AdminKey"] ? <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> :
<Box sx={{ cursor: "pointer" }} onClick={() => discordUrl ? window.location.assign(discordUrl) : console.log("no discord url :(")}><SignInIcon size={16} /></Box>
}
</Header>
);
}

16
src/components/Song.tsx Normal file
View File

@ -0,0 +1,16 @@
import { Box, Text } from "@primer/react";
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
export function Song({ children }: { children: JSX.Element[] }) {
return (
<Box sx={{ overflow: "hidden", minWidth: 50, maxWidth: 200, padding: 2, borderRadius: 10, border: "solid", borderColor: "border.default" }}>
<img src="https://cdn2.unrealengine.com/d5yc9zpe97um68u6-512x512-a7f5fc0d3c2f.png" style={{ width: "100%", borderRadius: 10 }} />
<center>
<Text sx={{ fontSize: 1, display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>Someone</Text>
<Text sx={{ display: "block", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>Someone's Song Someone's Song</Text>
<Divider />
</center>
{children}
</Box>
)
}

View File

@ -1,3 +1,8 @@
.content {
padding: 10px;
}
.songCategory {
display: inline-flex;
gap: 10px;
}

View File

@ -1,12 +1,14 @@
import { Box, Text } from "@primer/react";
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] = useCookies();
const [cookies, , removeCookie] = useCookies(); // how in the fuck is this valid ts syntax???????????
const navigate = useNavigate();
useEffect(() => {
(async() => setKeyValid((await VerifyAdminKey(cookies["AdminKey"])).Success))();
@ -20,6 +22,7 @@ export function AdminHome() {
</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>
</PageHeader.Description>
</PageHeader>
</Box>

View File

@ -1,5 +1,5 @@
import { Box, Button, Text, TextInput } from "@primer/react";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { VerifyAdminKey } from "../utils/AdminUtil";
import { useCookies } from "react-cookie";
import { useNavigate } from "react-router-dom";
@ -7,10 +7,16 @@ import { useNavigate } from "react-router-dom";
export function AdminLogin() {
const [errorMessage, setErrorMessage] = useState("");
const [success, setSuccess] = useState(false);
const [_, setCookie, removeCookie] = useCookies();
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>

6
src/routes/Download.tsx Normal file
View File

@ -0,0 +1,6 @@
export function Download() {
return (
<>
</>
)
}

59
src/routes/Profile.tsx Normal file
View File

@ -0,0 +1,59 @@
import { Avatar, Box, Button, Heading, Text } from "@primer/react"
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
import { PageHeader } from "@primer/react/drafts";
import { useContext } from "react";
import { SiteContext } from "../utils/State";
import { useCookies } from "react-cookie";
import { useNavigate } from "react-router-dom";
import { Song } from "../components/Song";
export function Profile() {
const { state, setState } = useContext(SiteContext);
const [, , removeCookie] = useCookies();
const navigate = useNavigate();
return (
<>
{
state.UserDetails ?
<Box sx={{ marginLeft: "15%", marginRight: "15%" }}>
<PageHeader>
<PageHeader.TitleArea variant="large">
<PageHeader.LeadingVisual>
<Avatar src={state.UserDetails.Avatar} size={32} alt={`${state.UserDetails.GlobalName} (@${state.UserDetails.Username})`} />
</PageHeader.LeadingVisual>
<PageHeader.Title>
{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>
</PageHeader.Actions>
</PageHeader.TitleArea>
</PageHeader>
<Divider />
<Heading sx={{ marginBottom: 2 }}>Active Songs</Heading>
<Box className="songCategory">
<Song>
<center>Overriding: <Text sx={{ display: "block", fontWeight: "700", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>Party Rock Anthem</Text></center>
<Button sx={{ width: "100%", marginTop: 2 }} variant="danger">Remove from Active</Button>
</Song>
</Box>
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>My Bookmarks</Heading>
<Box className="songCategory">
<Song>
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary">Add to Active</Button>
<Button sx={{ width: "100%" }} variant="danger">Remove from Bookmarks</Button>
</Song>
<Song>
<Button sx={{ width: "100%", marginBottom: 1 }} disabled>Add to Active</Button>
<Button sx={{ width: "100%" }} variant="danger">Remove from Bookmarks</Button>
</Song>
</Box>
</Box> :
<>
<Text>You are not logged in.<br />Log in using the button in the top right.</Text>
</>
}
</>
)
}

15
src/utils/State.ts Normal file
View File

@ -0,0 +1,15 @@
// massive shoutout to not-nullptr for the site state code from our old project
import { createContext } from "react";
export const SiteContext = createContext<IContext>({} as IContext);
export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string }
export interface SiteState {
UserDetails: UserDetailInterface | null
}
export interface IContext {
state: SiteState;
setState: (newState: SiteState) => void;
}

View File

@ -1,7 +1,19 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
resolve: {
alias: {
querystring: "querystring-es3",
Buffer: "buffer/"
}
},
plugins: [
react(),
NodeGlobalsPolyfillPlugin({
buffer: true,
}),
],
});