I LOVE GD COLOGNE
This commit is contained in:
parent
677281cbf3
commit
0588183828
7
.env
Normal file
7
.env
Normal file
|
@ -0,0 +1,7 @@
|
|||
#
|
||||
# IMPORTANT!
|
||||
# COPY THIS FILE AND NAME IT ".env.local" TO CREATE THE NON-GITHUB TRACKED VERSION!
|
||||
# CHANGE ALL THE VALUES ACCORDINGLY AND SAVE!
|
||||
#
|
||||
|
||||
VITE_SERVER_ROOT_URL=https://partypack.mcthe.dev/
|
7
.env.development
Normal file
7
.env.development
Normal file
|
@ -0,0 +1,7 @@
|
|||
#
|
||||
# IMPORTANT!
|
||||
# COPY THIS FILE AND NAME IT ".env.development.local" TO CREATE THE NON-GITHUB TRACKED VERSION!
|
||||
# CHANGE ALL THE VALUES ACCORDINGLY AND SAVE!
|
||||
#
|
||||
|
||||
VITE_SERVER_ROOT_URL=http://localhost:6677/
|
7
.env.production
Normal file
7
.env.production
Normal file
|
@ -0,0 +1,7 @@
|
|||
#
|
||||
# IMPORTANT!
|
||||
# COPY THIS FILE AND NAME IT ".env.production.local" TO CREATE THE NON-GITHUB TRACKED VERSION!
|
||||
# CHANGE ALL THE VALUES ACCORDINGLY AND SAVE!
|
||||
#
|
||||
|
||||
VITE_SERVER_ROOT_URL=https://partypack.mcthe.dev/
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -11,6 +11,7 @@ node_modules
|
|||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env.staging
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
@ -23,4 +24,6 @@ dist-ssr
|
|||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.private.*
|
||||
*.private.*
|
||||
|
||||
Out/
|
|
@ -1,12 +1,12 @@
|
|||
PORT=6677
|
||||
BODY_SIZE_LIMIT=50mb
|
||||
PROJECT_NAME=Partypack Service
|
||||
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=
|
||||
PORT=6677
|
||||
BODY_SIZE_LIMIT=50mb
|
||||
PROJECT_NAME=Partypack Service
|
||||
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
|
2
Server/.gitignore
vendored
2
Server/.gitignore
vendored
|
@ -74,6 +74,8 @@ web_modules/
|
|||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.staging
|
||||
.env.prod
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# BasedServer - mc's server base
|
||||
Hey! Welcome back to mc's yearly repo dump! Today, I'm making my SERVER BASE public for everyone to use. \
|
||||
This template gets rid of the need to write all the boilerplate server code, with an example route included:
|
||||
|
||||
![firefox_xQtTduHfDq](https://github.com/McMistrzYT/BasedServer/assets/56406996/3470b5d7-ca06-4aa2-89a6-d57367420e8d)
|
||||
|
||||
Have fun, 'cause you aren't getting any help!
|
||||
# BasedServer - mc's server base
|
||||
Hey! Welcome back to mc's yearly repo dump! Today, I'm making my SERVER BASE public for everyone to use. \
|
||||
This template gets rid of the need to write all the boilerplate server code, with an example route included:
|
||||
|
||||
![firefox_xQtTduHfDq](https://github.com/McMistrzYT/BasedServer/assets/56406996/3470b5d7-ca06-4aa2-89a6-d57367420e8d)
|
||||
|
||||
Have fun, 'cause you aren't getting any help!
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import { DataSource } from "typeorm";
|
||||
import { ENVIRONMENT } from "../Modules/Constants";
|
||||
import { join } from "path";
|
||||
import { Song } from "../Schemas/Song";
|
||||
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
||||
import { User } from "../Schemas/User";
|
||||
import { Rating } from "../Schemas/Rating";
|
||||
|
||||
export const DBSource = new DataSource({
|
||||
type: "better-sqlite3",
|
||||
database: `Partypack${ENVIRONMENT !== "prod" ? `-${ENVIRONMENT}` : ""}.db`,
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [join(__dirname, "..", "Schemas") + "\\*{.js,.ts}"],
|
||||
entities: [
|
||||
Song,
|
||||
ForcedCategory,
|
||||
User,
|
||||
Rating
|
||||
/*join(__dirname, "..", "Schemas") + "\\*{.js,.ts}"*/ // does not work in prod
|
||||
],
|
||||
subscribers: [],
|
||||
migrations: [],
|
||||
enableWAL: true
|
||||
|
|
|
@ -1,62 +1,74 @@
|
|||
import e from "express"
|
||||
import fs from "fs";
|
||||
import { BODY_SIZE_LIMIT, COOKIE_SIGN_KEY, DASHBOARD_ROOT, ENDPOINT_AUTHENTICATION_ENABLED, ENDPOINT_AUTH_HEADER, ENDPOINT_AUTH_VALUE, IS_DEBUG, PORT, PROJECT_NAME, SERVER_URL } from "../Modules/Constants";
|
||||
import { Debug, Msg, Warn } from "../Modules/Logger";
|
||||
import { italic, magenta, red, yellow } from "colorette";
|
||||
import cookieParser from "cookie-parser";
|
||||
import path from "path";
|
||||
import cors from "cors";
|
||||
|
||||
export const App = e()
|
||||
.disable("etag")
|
||||
.disable("x-powered-by")
|
||||
.use(cookieParser(COOKIE_SIGN_KEY))
|
||||
.use(cors({
|
||||
origin: DASHBOARD_ROOT,
|
||||
credentials: true
|
||||
}))
|
||||
.use(e.json({ limit: BODY_SIZE_LIMIT }))
|
||||
.use(e.urlencoded({ limit: BODY_SIZE_LIMIT, extended: false }));
|
||||
|
||||
async function Initialize() {
|
||||
const Files = fs
|
||||
.readdirSync(path.join(".", Symbol.for("ts-node.register.instance") in process ? "Source" : "bin", "Routes"))
|
||||
.filter((F) => F.endsWith(".js") || F.endsWith(".ts"));
|
||||
|
||||
if (ENDPOINT_AUTHENTICATION_ENABLED)
|
||||
Warn(`Endpoint authentication requirement is enabled! You will not be able to connect to the server without the ${yellow(ENDPOINT_AUTH_HEADER as string)} header.`)
|
||||
|
||||
App.use((req, res, next) => {
|
||||
if (ENDPOINT_AUTHENTICATION_ENABLED && req.header(ENDPOINT_AUTH_HEADER as string) !== ENDPOINT_AUTH_VALUE)
|
||||
return res.status(403).send(`${SERVER_URL} is currently locked behind authentication. Come back later!`);
|
||||
|
||||
Debug(req.path);
|
||||
next();
|
||||
})
|
||||
|
||||
for (const File of Files) {
|
||||
const Contents = await import(path.join("..", "Routes", File));
|
||||
if (!Contents.default) continue;
|
||||
if (!Contents.default.App) continue;
|
||||
App.use(Contents.default.DefaultAPI || "/", Contents.default.App);
|
||||
|
||||
Msg(`Loaded route ${italic(File)}!`);
|
||||
}
|
||||
|
||||
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)") : "")}`));
|
||||
}
|
||||
|
||||
Initialize();
|
||||
|
||||
// ! FESTIVAL-SPECIFIC STUFF
|
||||
import axios from "axios";
|
||||
|
||||
axios.defaults.validateStatus = () => true;
|
||||
import e from "express"
|
||||
import fs, { existsSync, mkdirSync } from "fs";
|
||||
import { BODY_SIZE_LIMIT, COOKIE_SIGN_KEY, DASHBOARD_ROOT, ENDPOINT_AUTHENTICATION_ENABLED, ENDPOINT_AUTH_HEADER, ENDPOINT_AUTH_VALUE, IS_DEBUG, PORT, PROJECT_NAME, SERVER_URL } from "../Modules/Constants";
|
||||
import { Debug, Msg, Warn } from "../Modules/Logger";
|
||||
import { italic, magenta, red, yellow } from "colorette";
|
||||
import cookieParser from "cookie-parser";
|
||||
import path from "path";
|
||||
import cors from "cors";
|
||||
|
||||
export const App = e()
|
||||
.disable("etag")
|
||||
.disable("x-powered-by")
|
||||
.use(cookieParser(COOKIE_SIGN_KEY))
|
||||
.use(cors({
|
||||
origin: DASHBOARD_ROOT,
|
||||
credentials: true
|
||||
}))
|
||||
.use(e.json({ limit: BODY_SIZE_LIMIT }))
|
||||
.use(e.urlencoded({ limit: BODY_SIZE_LIMIT, extended: false }));
|
||||
|
||||
async function Initialize() {
|
||||
if (!existsSync("./Saved") || !existsSync("./Saved/Songs"))
|
||||
mkdirSync("./Saved/Songs", { recursive: true });
|
||||
|
||||
Debug(`The CWD is ${magenta(process.cwd())}.`);
|
||||
|
||||
const Files = fs
|
||||
.readdirSync(path.join(".", Symbol.for("ts-node.register.instance") in process ? "Source" : "bin", "Routes"))
|
||||
.filter((F) => F.endsWith(".js") || F.endsWith(".ts"));
|
||||
|
||||
if (ENDPOINT_AUTHENTICATION_ENABLED)
|
||||
Warn(`Endpoint authentication requirement is enabled! You will not be able to connect to the server without the ${yellow(ENDPOINT_AUTH_HEADER as string)} header.`)
|
||||
|
||||
App.use((req, res, next) => {
|
||||
if (ENDPOINT_AUTHENTICATION_ENABLED && req.header(ENDPOINT_AUTH_HEADER as string) !== ENDPOINT_AUTH_VALUE)
|
||||
return res.status(403).send(`${SERVER_URL} is currently locked behind authentication. Come back later!`);
|
||||
|
||||
Debug(req.path);
|
||||
next();
|
||||
})
|
||||
|
||||
for (const File of Files) {
|
||||
const Contents = await import(path.join("..", "Routes", File));
|
||||
if (!Contents.default) continue;
|
||||
if (!Contents.default.App) continue;
|
||||
App.use(Contents.default.DefaultAPI || "/", Contents.default.App);
|
||||
|
||||
Msg(`Loaded route ${italic(File)}!`);
|
||||
}
|
||||
|
||||
if (existsSync("./dist")) {
|
||||
Warn(`Detected ${yellow("dist")} folder! Using as static.`);
|
||||
App.use("/", e.static("dist"));
|
||||
App.use("/assets", e.static("dist/assets"));
|
||||
}
|
||||
|
||||
App.use("/api/*", (_, res) => res.status(404).json({ errorMessage: "Not Found" }));
|
||||
App.use("*", (_, res) => res.sendFile(path.join(process.cwd(), "dist", "index.html")));
|
||||
|
||||
App.use((err, req, res, _) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ errorMessage: IS_DEBUG ? err.message : "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)") : "")}`));
|
||||
}
|
||||
|
||||
Initialize();
|
||||
|
||||
// ! FESTIVAL-SPECIFIC STUFF
|
||||
import axios from "axios";
|
||||
|
||||
axios.defaults.validateStatus = () => true;
|
||||
axios.defaults.headers.common["X-Do-Not-Redirect"] = "true";
|
|
@ -1,21 +1,21 @@
|
|||
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.
|
||||
export const DASHBOARD_URL = process.env.DASHBOARD_URL ?? "localhost:5173"; // The server's URL. Not used for a lot by default.
|
||||
export const PORT = process.env.PORT ?? 6677; // Port for the server to run on.
|
||||
export const ENDPOINT_AUTHENTICATION_ENABLED = !!process.env.ENDPOINT_AUTHENTICATION; // Whether the server is locked down behind a header.
|
||||
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 = !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 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 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.
|
||||
export const DASHBOARD_URL = process.env.DASHBOARD_URL ?? "localhost:5173"; // The server's URL. Not used for a lot by default.
|
||||
export const PORT = process.env.PORT ?? 6677; // Port for the server to run on.
|
||||
export const ENDPOINT_AUTHENTICATION_ENABLED = !!process.env.ENDPOINT_AUTHENTICATION; // Whether the server is locked down behind a header.
|
||||
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 = process.env.USE_HTTPS?.toLowerCase() === "true";
|
||||
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}${!USE_HTTPS ? `:${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 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.
|
|
@ -1,141 +1,141 @@
|
|||
import axios from "axios";
|
||||
import { Err } from "./Logger";
|
||||
import { red } from "colorette";
|
||||
import { FULL_SERVER_ROOT } from "./Constants";
|
||||
import { User } from "../Schemas/User";
|
||||
import { Song } from "../Schemas/Song";
|
||||
|
||||
export let FullFortnitePages: {[key: string]: any} | null = null;
|
||||
export let OriginalSparks: {[key: string]: any} | null = null;
|
||||
let LastContentDownloadDate: Date = new Date(0); // set it to 1970 as default cuz im not boutta check if its null
|
||||
|
||||
GenerateFortnitePages(null);
|
||||
|
||||
export async function GenerateFortnitePages(ForUser: User | null): Promise<{ Success: boolean, FNPages: { [key: string]: unknown } | null }> {
|
||||
const { status, data } = // check if 30 minutes have passed since last content update. if so, get a new copy of pages, if not, fuck off
|
||||
FullFortnitePages === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ?
|
||||
await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") :
|
||||
{ status: 200, data: FullFortnitePages };
|
||||
|
||||
const OGSparks =
|
||||
OriginalSparks === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ?
|
||||
await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/spark-tracks") :
|
||||
{ status: 200, data: OriginalSparks };
|
||||
|
||||
FullFortnitePages = {
|
||||
...data,
|
||||
sparkTracks: {
|
||||
...data.sparkTracks,
|
||||
lastModified: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
OriginalSparks = OGSparks.data;
|
||||
LastContentDownloadDate = new Date();
|
||||
|
||||
if (!ForUser)
|
||||
return { Success: true, FNPages: null };
|
||||
|
||||
if (status !== 200 || OGSparks.status !== 200) {
|
||||
Err(`Failed to get Fortnite pages: ${red(status)}, ${red(OGSparks.status)}`);
|
||||
console.log(data);
|
||||
process.exit(-1); // very big fuck moment, we literally cannot run the server without fortnitepages
|
||||
}
|
||||
|
||||
const AllSongs: { [key: string]: unknown } = {}; // too lazy to actually write a schema for this :D
|
||||
const Overrides = ForUser.Library.map(x => { return { ...x, SongData: Song.findOne({ where: { ID: x.SongID } }) }; });
|
||||
const UsersLibrary = await Promise.all(Overrides.map(x => x.SongData));
|
||||
|
||||
for (const Song of UsersLibrary)
|
||||
{
|
||||
if (!Song)
|
||||
continue;
|
||||
|
||||
const OverridingAs = Overrides.find(x => x.SongID === Song.ID);
|
||||
if (!OverridingAs)
|
||||
continue;
|
||||
|
||||
const OriginalTrack = Object.values(OriginalSparks!).find(x => x.track?.ti === `SparksSong:${OverridingAs.Overriding.toLowerCase()}`);
|
||||
if (!OriginalTrack)
|
||||
continue;
|
||||
|
||||
AllSongs[OriginalTrack._title] = {
|
||||
_title: OriginalTrack._title,
|
||||
_noIndex: false,
|
||||
_activeDate: "2023-01-01T01:00:00.000Z",
|
||||
_locale: "en-US",
|
||||
_templateName: "track",
|
||||
lastModified: new Date().toISOString(),
|
||||
track: {
|
||||
tt: Song.Name, // tt - Title,
|
||||
an: Song.ArtistName, // an - Artist Name
|
||||
mm: Song.Scale, // mm - Minor, Major
|
||||
mk: Song.Key, // mk - Music Key
|
||||
ab: Song.Album, // ab - Album
|
||||
su: OriginalTrack._title, // su - Song UUID
|
||||
ry: Song.Year, // ry - Release Year
|
||||
mt: Song.Tempo, // mt - Music Timing (?)
|
||||
au: Song.Cover ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/cover.png`, // au - Album Cover
|
||||
gt: [ "Jam-LoopIsUnpitched-Beat" ], // gt - Gameplay Tags (but in a different format: Example.Gameplay.Tag -> Example-Gameplay-Tag)
|
||||
ti: `SparksSong:${OverridingAs.Overriding.toLowerCase()}`,
|
||||
mu: Song.Midi ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/midi.mid`, // mu - Song Midi (encrypted)
|
||||
dn: Song.Length, // dn - Track Length (in seconds)
|
||||
ge: [ "Pop" ], // ge - Genres
|
||||
in: {
|
||||
ba: Song.BassDifficulty,
|
||||
pb: Song.BassDifficulty,
|
||||
pd: Song.DrumsDifficulty,
|
||||
ds: Song.DrumsDifficulty,
|
||||
pg: Song.GuitarDifficulty,
|
||||
gr: Song.GuitarDifficulty,
|
||||
vl: Song.VocalsDifficulty,
|
||||
_type: "SparkTrackIntensities"
|
||||
}, // in - Intensities (those white bars you see)
|
||||
sib: "Bass", // sib - Bass ID to use (only Bass possible)
|
||||
sid: "Drum", // sid - Drums ID to use (only Drum possible)
|
||||
sig: Song.GuitarStarterType, // sig - Guitar ID to use (Keytar/Guitar)
|
||||
siv: "Vocals", // siv - Vocals ID to use (only Vocals possible)
|
||||
qi: JSON.stringify({ // qi - Query Information (frontend related display stuff and language vocals channel related stuff)
|
||||
sid: Song.ID, // sid - Song UUID
|
||||
pid: Song.ID, // pid - Playlist Asset ID
|
||||
title: OriginalTrack._title, // title - Song Name - same as _title
|
||||
tracks: [
|
||||
{
|
||||
part: "ds", // Drum Set
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "bs", // Bass Set (not bullshit)
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "gs", // Guitar Set
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "vs", // Vocal Set (not Visual Studio)
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "fs", // Fart Set (jk i have no idea)
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
}
|
||||
],
|
||||
preview: {
|
||||
starttime: 0
|
||||
}
|
||||
}),
|
||||
ld: Song.Lipsync ?? OriginalTrack.track.ld, // ld - Lipsync Data (it's literally a uasset)
|
||||
jc: OriginalTrack.track.jc, // jc - Join Code (UEFN empty island with nothing - possibly downloads assets)
|
||||
sn: OriginalTrack._title, // sn - Song Name - same as _title
|
||||
_type: "SparkTrack"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { Success: true, FNPages: AllSongs };
|
||||
import axios from "axios";
|
||||
import { Err } from "./Logger";
|
||||
import { red } from "colorette";
|
||||
import { FULL_SERVER_ROOT } from "./Constants";
|
||||
import { User } from "../Schemas/User";
|
||||
import { Song } from "../Schemas/Song";
|
||||
|
||||
export let FullFortnitePages: {[key: string]: any} | null = null;
|
||||
export let OriginalSparks: {[key: string]: any} | null = null;
|
||||
let LastContentDownloadDate: Date = new Date(0); // set it to 1970 as default cuz im not boutta check if its null
|
||||
|
||||
GenerateFortnitePages(null);
|
||||
|
||||
export async function GenerateFortnitePages(ForUser: User | null): Promise<{ Success: boolean, FNPages: { [key: string]: unknown } | null }> {
|
||||
const { status, data } = // check if 30 minutes have passed since last content update. if so, get a new copy of pages, if not, fuck off
|
||||
FullFortnitePages === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ?
|
||||
await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game") :
|
||||
{ status: 200, data: FullFortnitePages };
|
||||
|
||||
const OGSparks =
|
||||
OriginalSparks === null || Date.now() > LastContentDownloadDate.getTime() + 30 * 60 * 1000 ?
|
||||
await axios.get("https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/spark-tracks") :
|
||||
{ status: 200, data: OriginalSparks };
|
||||
|
||||
FullFortnitePages = {
|
||||
...data,
|
||||
sparkTracks: {
|
||||
...data.sparkTracks,
|
||||
lastModified: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
OriginalSparks = OGSparks.data;
|
||||
LastContentDownloadDate = new Date();
|
||||
|
||||
if (!ForUser)
|
||||
return { Success: true, FNPages: null };
|
||||
|
||||
if (status !== 200 || OGSparks.status !== 200) {
|
||||
Err(`Failed to get Fortnite pages: ${red(status)}, ${red(OGSparks.status)}`);
|
||||
console.log(data);
|
||||
process.exit(-1); // very big fuck moment, we literally cannot run the server without fortnitepages
|
||||
}
|
||||
|
||||
const AllSongs: { [key: string]: unknown } = {}; // too lazy to actually write a schema for this :D
|
||||
const Overrides = ForUser.Library.map(x => { return { ...x, SongData: Song.findOne({ where: { ID: x.SongID } }) }; });
|
||||
const UsersLibrary = await Promise.all(Overrides.map(x => x.SongData));
|
||||
|
||||
for (const Song of UsersLibrary)
|
||||
{
|
||||
if (!Song)
|
||||
continue;
|
||||
|
||||
const OverridingAs = Overrides.find(x => x.SongID === Song.ID);
|
||||
if (!OverridingAs)
|
||||
continue;
|
||||
|
||||
const OriginalTrack = Object.values(OriginalSparks!).find(x => x.track?.ti === `SparksSong:${OverridingAs.Overriding.toLowerCase()}`);
|
||||
if (!OriginalTrack)
|
||||
continue;
|
||||
|
||||
AllSongs[OriginalTrack._title] = {
|
||||
_title: OriginalTrack._title,
|
||||
_noIndex: false,
|
||||
_activeDate: "2023-01-01T01:00:00.000Z",
|
||||
_locale: "en-US",
|
||||
_templateName: "track",
|
||||
lastModified: new Date().toISOString(),
|
||||
track: {
|
||||
tt: Song.Name, // tt - Title,
|
||||
an: Song.ArtistName, // an - Artist Name
|
||||
mm: Song.Scale, // mm - Minor, Major
|
||||
mk: Song.Key, // mk - Music Key
|
||||
ab: Song.Album, // ab - Album
|
||||
su: OriginalTrack._title, // su - Song UUID
|
||||
ry: Song.Year, // ry - Release Year
|
||||
mt: Song.Tempo, // mt - Music Timing (?)
|
||||
au: Song.Cover ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/cover.png`, // au - Album Cover
|
||||
gt: [ "Jam-LoopIsUnpitched-Beat" ], // gt - Gameplay Tags (but in a different format: Example.Gameplay.Tag -> Example-Gameplay-Tag)
|
||||
ti: `SparksSong:${OverridingAs.Overriding.toLowerCase()}`,
|
||||
mu: Song.Midi ?? `${FULL_SERVER_ROOT}/song/download/${Song.ID}/midi.mid`, // mu - Song Midi (encrypted)
|
||||
dn: Song.Length, // dn - Track Length (in seconds)
|
||||
ge: [ "Pop" ], // ge - Genres
|
||||
in: {
|
||||
ba: Song.BassDifficulty,
|
||||
pb: Song.BassDifficulty,
|
||||
pd: Song.DrumsDifficulty,
|
||||
ds: Song.DrumsDifficulty,
|
||||
pg: Song.GuitarDifficulty,
|
||||
gr: Song.GuitarDifficulty,
|
||||
vl: Song.VocalsDifficulty,
|
||||
_type: "SparkTrackIntensities"
|
||||
}, // in - Intensities (those white bars you see)
|
||||
sib: "Bass", // sib - Bass ID to use (only Bass possible)
|
||||
sid: "Drum", // sid - Drums ID to use (only Drum possible)
|
||||
sig: Song.GuitarStarterType, // sig - Guitar ID to use (Keytar/Guitar)
|
||||
siv: "Vocals", // siv - Vocals ID to use (only Vocals possible)
|
||||
qi: JSON.stringify({ // qi - Query Information (frontend related display stuff and language vocals channel related stuff)
|
||||
sid: Song.ID, // sid - Song UUID
|
||||
pid: Song.ID, // pid - Playlist Asset ID
|
||||
title: OriginalTrack._title, // title - Song Name - same as _title
|
||||
tracks: [
|
||||
{
|
||||
part: "ds", // Drum Set
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "bs", // Bass Set (not bullshit)
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "gs", // Guitar Set
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "vs", // Vocal Set (not Visual Studio)
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
},
|
||||
{
|
||||
part: "fs", // Fart Set (jk i have no idea)
|
||||
channels: [ "FL", "FR" ],
|
||||
vols: [ 4, 4 ]
|
||||
}
|
||||
],
|
||||
preview: {
|
||||
starttime: 0
|
||||
}
|
||||
}),
|
||||
ld: Song.Lipsync ?? OriginalTrack.track.ld, // ld - Lipsync Data (it's literally a uasset)
|
||||
jc: OriginalTrack.track.jc, // jc - Join Code (UEFN empty island with nothing - possibly downloads assets)
|
||||
sn: OriginalTrack._title, // sn - Song Name - same as _title
|
||||
_type: "SparkTrack"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { Success: true, FNPages: AllSongs };
|
||||
}
|
|
@ -1,19 +1,19 @@
|
|||
import { green, gray, red, magenta, yellow } from "colorette";
|
||||
import { IS_DEBUG, PROJECT_NAME } from "./Constants";
|
||||
|
||||
export function Msg(Content: string, Prefix = PROJECT_NAME) {
|
||||
console.log(`${gray(new Date().toISOString())} [${green(Prefix)}] ${Content}`);
|
||||
}
|
||||
|
||||
export function Err(Content: string, Prefix = PROJECT_NAME) {
|
||||
console.log(`${gray(new Date().toISOString())} [${red("ERROR | " + Prefix)}] ${Content}`);
|
||||
}
|
||||
|
||||
export function Warn(Content: string, Prefix = PROJECT_NAME) {
|
||||
console.log(`${gray(new Date().toISOString())} [${yellow("WARNING | " + Prefix)}] ${Content}`);
|
||||
}
|
||||
|
||||
export function Debug(Content: string, Prefix = PROJECT_NAME) {
|
||||
if (!IS_DEBUG) return;
|
||||
console.log(`${gray(new Date().toISOString())} [${magenta("DEBUG | " + Prefix)}] ${Content}`);
|
||||
}
|
||||
import { green, gray, red, magenta, yellow } from "colorette";
|
||||
import { IS_DEBUG, PROJECT_NAME } from "./Constants";
|
||||
|
||||
export function Msg(Content: string, Prefix = PROJECT_NAME) {
|
||||
console.log(`${gray(new Date().toISOString())} [${green(Prefix)}] ${Content}`);
|
||||
}
|
||||
|
||||
export function Err(Content: string, Prefix = PROJECT_NAME) {
|
||||
console.log(`${gray(new Date().toISOString())} [${red("ERROR | " + Prefix)}] ${Content}`);
|
||||
}
|
||||
|
||||
export function Warn(Content: string, Prefix = PROJECT_NAME) {
|
||||
console.log(`${gray(new Date().toISOString())} [${yellow("WARNING | " + Prefix)}] ${Content}`);
|
||||
}
|
||||
|
||||
export function Debug(Content: string, Prefix = PROJECT_NAME) {
|
||||
if (!IS_DEBUG) return;
|
||||
console.log(`${gray(new Date().toISOString())} [${magenta("DEBUG | " + Prefix)}] ${Content}`);
|
||||
}
|
||||
|
|
|
@ -1,47 +1,47 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import { User } from "../Schemas/User";
|
||||
import { JwtPayload, verify } from "jsonwebtoken";
|
||||
import { IS_DEBUG, JWT_KEY } from "./Constants";
|
||||
import j from "joi";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function RequireAuthentication(Relations?: object) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.header("X-Partypack-Token") && !req.cookies["Token"] && !req.header("Authorization"))
|
||||
return res.status(401).json({ errorMessage: "This endpoint requires authorization." });
|
||||
|
||||
let JWT: JwtPayload;
|
||||
try {
|
||||
JWT = verify(req.header("X-Partypack-Token") ?? req.cookies["Token"] ?? req.header("Authorization"), JWT_KEY!) as JwtPayload;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(403).json({ errorMessage: `Invalid Partypack token provided.${IS_DEBUG ? ` (${err})` : ""}` });
|
||||
}
|
||||
|
||||
const UserData = await User.findOne({ where: { ID: JWT.ID }, relations: Relations });
|
||||
if (!UserData)
|
||||
return res.status(401).json({ errorMessage: "Invalid Partypack token provided. User does not exist in database. Please contact an instance admin." });
|
||||
|
||||
req.user = UserData;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export function ValidateBody(Schema: j.Schema) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
req.body = await Schema.validateAsync(req.body);
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(400).json({ errorMessage: "Body validation failed.", details: err })
|
||||
}
|
||||
}
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { User } from "../Schemas/User";
|
||||
import { JwtPayload, verify } from "jsonwebtoken";
|
||||
import { IS_DEBUG, JWT_KEY } from "./Constants";
|
||||
import j from "joi";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function RequireAuthentication(Relations?: object) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.header("X-Partypack-Token") && !req.cookies["Token"] && !req.header("Authorization"))
|
||||
return res.status(401).json({ errorMessage: "This endpoint requires authorization." });
|
||||
|
||||
let JWT: JwtPayload;
|
||||
try {
|
||||
JWT = verify(req.header("X-Partypack-Token") ?? req.cookies["Token"] ?? req.header("Authorization"), JWT_KEY!) as JwtPayload;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.status(403).json({ errorMessage: `Invalid Partypack token provided.${IS_DEBUG ? ` (${err})` : ""}` });
|
||||
}
|
||||
|
||||
const UserData = await User.findOne({ where: { ID: JWT.ID }, relations: Relations });
|
||||
if (!UserData)
|
||||
return res.status(401).json({ errorMessage: "Invalid Partypack token provided. User does not exist in database. Please contact an instance admin." });
|
||||
|
||||
req.user = UserData;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
export function ValidateBody(Schema: j.Schema) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
req.body = await Schema.validateAsync(req.body);
|
||||
next();
|
||||
} catch (err) {
|
||||
res.status(400).json({ errorMessage: "Body validation failed.", details: err })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,166 +1,176 @@
|
|||
/* eslint-disable no-case-declarations */
|
||||
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
||||
import { Router } from "express";
|
||||
import { UserPermissions } from "../Schemas/User";
|
||||
import { Song } from "../Schemas/Song";
|
||||
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";
|
||||
|
||||
const App = Router();
|
||||
|
||||
// ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION !
|
||||
// ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION !
|
||||
// ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION !
|
||||
|
||||
App.use(RequireAuthentication());
|
||||
|
||||
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("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package())));
|
||||
|
||||
App.post("/create/song",
|
||||
ValidateBody(j.object({
|
||||
ID: j.string().uuid(),
|
||||
Name: j.string().required().min(3).max(64),
|
||||
Year: j.number().required().min(1).max(2999),
|
||||
ArtistName: j.string().required().min(1).max(64),
|
||||
Length: j.number().required().min(1),
|
||||
Scale: j.string().valid("Minor", "Major").required(),
|
||||
Key: j.string().valid("A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb").required(),
|
||||
Album: j.string().required(),
|
||||
GuitarStarterType: j.string().valid("Keytar", "Guitar").required(),
|
||||
Tempo: j.number().min(20).max(1250).required(),
|
||||
Midi: j.string().uri(),
|
||||
Cover: j.string().uri(),
|
||||
Lipsync: j.string().uri(),
|
||||
BassDifficulty: j.number().required().min(0).max(7),
|
||||
GuitarDifficulty: j.number().required().min(0).max(7),
|
||||
DrumsDifficulty: j.number().required().min(0).max(7),
|
||||
VocalsDifficulty: j.number().required().min(0).max(7)
|
||||
})),
|
||||
async (req, res) => {
|
||||
res.json(await Song.create(req.body).save())
|
||||
});
|
||||
|
||||
App.post("/upload/midi",
|
||||
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 (!Decoded.toString().startsWith("MThd"))
|
||||
return res.status(400).send("Uploaded MIDI file is not a valid MIDI.");
|
||||
|
||||
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
|
||||
return res.status(404).send("The song you're trying to upload a MIDI for does not exist.");
|
||||
|
||||
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded);
|
||||
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(),
|
||||
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 a cover for does not exist.");
|
||||
|
||||
try { // todo: fix
|
||||
/*const ImageMetadata = exif(Decoded);
|
||||
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
|
||||
throw new Error("Invalid image file.");
|
||||
|
||||
if (ImageMetadata.Image.ImageWidth !== ImageMetadata.Image.ImageLength)
|
||||
return res.status(400).send("Image must have a 1:1 ratio.");
|
||||
|
||||
if (ImageMetadata.Image.ImageWidth < 512 || ImageMetadata.Image.ImageWidth > 2048)
|
||||
return res.status(400).send("Image cannot be smaller than 512 pixels and larger than 2048 pixels.");*/
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return res.status(400).send("Invalid image file.");
|
||||
}
|
||||
|
||||
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Cover.png`, Decoded);
|
||||
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"
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
||||
import { Router } from "express";
|
||||
import { UserPermissions } from "../Schemas/User";
|
||||
import { Song } from "../Schemas/Song";
|
||||
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";
|
||||
|
||||
const App = Router();
|
||||
|
||||
// ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION !
|
||||
// ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION !
|
||||
// ! ANY ENDPOINTS DEFINED IN THIS FILE WILL REQUIRE ADMIN AUTHORIZATION !
|
||||
|
||||
App.use(RequireAuthentication());
|
||||
|
||||
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("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package())));
|
||||
|
||||
App.post("/create/song",
|
||||
ValidateBody(j.object({
|
||||
ID: j.string().uuid(),
|
||||
Name: j.string().required().min(3).max(64),
|
||||
Year: j.number().required().min(1).max(2999),
|
||||
ArtistName: j.string().required().min(1).max(64),
|
||||
Length: j.number().required().min(1),
|
||||
Scale: j.string().valid("Minor", "Major").required(),
|
||||
Key: j.string().valid("A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb").required(),
|
||||
Album: j.string().required(),
|
||||
GuitarStarterType: j.string().valid("Keytar", "Guitar").required(),
|
||||
Tempo: j.number().min(20).max(1250).required(),
|
||||
Midi: j.string().uri(),
|
||||
Cover: j.string().uri(),
|
||||
Lipsync: j.string().uri(),
|
||||
BassDifficulty: j.number().required().min(0).max(7),
|
||||
GuitarDifficulty: j.number().required().min(0).max(7),
|
||||
DrumsDifficulty: j.number().required().min(0).max(7),
|
||||
VocalsDifficulty: j.number().required().min(0).max(7)
|
||||
})),
|
||||
async (req, res) => {
|
||||
res.json(await Song.create(req.body).save())
|
||||
});
|
||||
|
||||
App.post("/upload/midi",
|
||||
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 (!Decoded.toString().startsWith("MThd"))
|
||||
return res.status(400).send("Uploaded MIDI file is not a valid MIDI.");
|
||||
|
||||
if (!await Song.exists({ where: { ID: req.body.TargetSong } }))
|
||||
return res.status(404).send("The song you're trying to upload a MIDI for does not exist.");
|
||||
|
||||
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded);
|
||||
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(),
|
||||
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 a cover for does not exist.");
|
||||
|
||||
try { // todo: fix
|
||||
/*const ImageMetadata = exif(Decoded);
|
||||
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
|
||||
throw new Error("Invalid image file.");
|
||||
|
||||
if (ImageMetadata.Image.ImageWidth !== ImageMetadata.Image.ImageLength)
|
||||
return res.status(400).send("Image must have a 1:1 ratio.");
|
||||
|
||||
if (ImageMetadata.Image.ImageWidth < 512 || ImageMetadata.Image.ImageWidth > 2048)
|
||||
return res.status(400).send("Image cannot be smaller than 512 pixels and larger than 2048 pixels.");*/
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return res.status(400).send("Invalid image file.");
|
||||
}
|
||||
|
||||
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Cover.png`, Decoded);
|
||||
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(),
|
||||
ShouldDelete: j.boolean().required()
|
||||
})).max(15)),
|
||||
async (req, res) => {
|
||||
const b = req.body as { ID: string, Songs: string[], Priority: number, Header: string, ShouldDelete: boolean }[];
|
||||
const Failures: { Regarding: string, Message: string }[] = [];
|
||||
const Successes: { Regarding: string, Message: string }[] = [];
|
||||
|
||||
for (const Entry of b) {
|
||||
let Category = await ForcedCategory.findOne({ where: { ID: Entry.ID } });
|
||||
if (Entry.ShouldDelete) { // DELETION
|
||||
if (!Category) {
|
||||
Failures.push({ Regarding: Entry.ID, Message: "Cannot delete non-existent category." });
|
||||
continue;
|
||||
}
|
||||
|
||||
await Category.remove();
|
||||
Successes.push({ Regarding: Entry.ID, Message: "Successfully deleted category." });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Category) // CREATION
|
||||
Category = await ForcedCategory.create({
|
||||
Header: Entry.Header,
|
||||
Activated: true,
|
||||
Priority: Entry.Priority,
|
||||
Songs: []
|
||||
});
|
||||
|
||||
// MODIFICATION
|
||||
const Songs = await Promise.all(Entry.Songs.map(x => Song.findOne({ where: { ID: x } })));
|
||||
if (Songs.includes(null)) {
|
||||
Failures.push({ Regarding: Entry.ID, Message: `Cannot modify "${Entry.ID}" songs as it includes a non-existent song` });
|
||||
continue;
|
||||
}
|
||||
|
||||
Category.Header = Entry.Header;
|
||||
Category.Priority = Entry.Priority;
|
||||
Category.Songs = Songs as Song[];
|
||||
Category.save();
|
||||
|
||||
Successes.push({ Regarding: Entry.ID, Message: `Successfully created/modified category "${Category.ID}".` });
|
||||
}
|
||||
|
||||
res.status(Failures.length > Successes.length ? 400 : 200).json({
|
||||
Failures,
|
||||
Successes
|
||||
})
|
||||
});
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/admin"
|
||||
}
|
|
@ -1,69 +1,69 @@
|
|||
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";
|
||||
import { User, UserPermissions } from "../Schemas/User";
|
||||
|
||||
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);
|
||||
|
||||
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`, 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"));
|
||||
if (Decoded.Client === "PartypackerDesktop")
|
||||
return res.redirect(`http://localhost:14968/?token=${encodeURI(JWT)}&user=${encodeURI(UserDetails)}`)
|
||||
else
|
||||
return res.status(400).send("Unsupported API client."); // idk maybe in the future we will maek more clients
|
||||
} catch {
|
||||
return res.status(400).send("Invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
.cookie("Token", JWT)
|
||||
.cookie("UserDetails", UserDetails)
|
||||
.redirect(`${DASHBOARD_ROOT}/profile`);
|
||||
})
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api"
|
||||
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";
|
||||
import { User, UserPermissions } from "../Schemas/User";
|
||||
|
||||
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);
|
||||
|
||||
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`, 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"));
|
||||
if (Decoded.Client === "PartypackerDesktop")
|
||||
return res.redirect(`http://localhost:14968/?token=${encodeURI(JWT)}&user=${encodeURI(UserDetails)}`)
|
||||
else
|
||||
return res.status(400).send("Unsupported API client."); // idk maybe in the future we will maek more clients
|
||||
} catch {
|
||||
return res.status(400).send("Invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
.cookie("Token", JWT)
|
||||
.cookie("UserDetails", UserDetails)
|
||||
.redirect(`${DASHBOARD_ROOT}/profile`);
|
||||
})
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api"
|
||||
}
|
|
@ -1,26 +1,26 @@
|
|||
import { Router } from "express";
|
||||
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
||||
import { Song } from "../Schemas/Song";
|
||||
|
||||
const App = Router();
|
||||
|
||||
App.get("/", async (req, res) => {
|
||||
const ForcedCategories = await ForcedCategory.find({ where: { Activated: true } });
|
||||
const New = {
|
||||
ID: "new",
|
||||
Header: "Recently added",
|
||||
Songs: (await Song.find({ take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()),
|
||||
Priority: 100,
|
||||
Custom: false
|
||||
}
|
||||
|
||||
res.json([
|
||||
...ForcedCategories.map(x => { return { ...x, Custom: true }; }),
|
||||
New
|
||||
].sort((a, b) => a.Priority - b.Priority))
|
||||
});
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/discovery"
|
||||
import { Router } from "express";
|
||||
import { ForcedCategory } from "../Schemas/ForcedCategory";
|
||||
import { Song } from "../Schemas/Song";
|
||||
|
||||
const App = Router();
|
||||
|
||||
App.get("/", async (req, res) => {
|
||||
const ForcedCategories = await ForcedCategory.find({ where: { Activated: true } });
|
||||
const New = {
|
||||
ID: "new",
|
||||
Header: "Recently added",
|
||||
Songs: (await Song.find({ take: 10, order: { CreationDate: "DESC" } })).map(x => x.Package()),
|
||||
Priority: 100,
|
||||
Custom: false
|
||||
}
|
||||
|
||||
res.json([
|
||||
...ForcedCategories.map(x => { return { ...x, Custom: true, Songs: x.Songs.map(y => y.Package()) }; }),
|
||||
New
|
||||
].sort((a, b) => a.Priority - b.Priority))
|
||||
});
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/discovery"
|
||||
}
|
|
@ -63,10 +63,10 @@ App.get("/song/download/:InternalID/:File", async (req, res) => {
|
|||
res.send(readFileSync(`${SongData.Directory}/Chunks/${req.params.File}`));
|
||||
});
|
||||
|
||||
App.get("/:InternalID", async (req, res) => {
|
||||
App.get("/:InternalID", async (req, res, next) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "Song not found." });
|
||||
return next(); // trust me bro
|
||||
|
||||
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`;
|
||||
res.set("content-type", "application/json");
|
||||
|
|
|
@ -1,99 +1,102 @@
|
|||
import { Router } from "express";
|
||||
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||
import { Song } from "../Schemas/Song";
|
||||
import { OriginalSparks } from "../Modules/FNUtil";
|
||||
import j from "joi";
|
||||
|
||||
const App = Router();
|
||||
|
||||
App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => {
|
||||
res.json({
|
||||
Bookmarks: req.user?.BookmarkedSongs.map(x => x.Package()),
|
||||
Library: req.user?.Library
|
||||
})
|
||||
})
|
||||
|
||||
App.post("/me/activate",
|
||||
RequireAuthentication(),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required(),
|
||||
ToOverride: j.string().pattern(/^sid_placeholder_(\d){1,3}$/i).required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
if (req.user?.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase() || x.Overriding.toLowerCase() === req.body.ToOverride.toLowerCase()) !== -1)
|
||||
return res.status(400).json({ errorMessage: "This song is already activated." });
|
||||
|
||||
if (!await Song.exists({ where: { ID: req.body.SongID } }))
|
||||
return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
|
||||
|
||||
req.user?.Library.push({ SongID: req.body.SongID.toLowerCase(), Overriding: req.body.ToOverride.toLowerCase() });
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.Library);
|
||||
})
|
||||
|
||||
App.post("/me/deactivate",
|
||||
RequireAuthentication(),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
const idx = req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase());
|
||||
if (idx === -1)
|
||||
return res.status(400).json({ errorMessage: "This song is not activated." });
|
||||
|
||||
req.user?.Library.splice(idx, 1);
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.Library);
|
||||
})
|
||||
|
||||
App.post("/me/bookmark",
|
||||
RequireAuthentication({ BookmarkedSongs: true }),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
if (req.user?.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()) !== -1)
|
||||
return res.status(400).json({ errorMessage: "You're already subscribed to this song." });
|
||||
|
||||
const SongData = await Song.findOne({ where: { ID: req.body.SongID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
|
||||
|
||||
req.user?.BookmarkedSongs.push(SongData);
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
|
||||
})
|
||||
|
||||
App.post("/me/unbookmark",
|
||||
RequireAuthentication(),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required()
|
||||
})),
|
||||
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 aren't subscribed to this song." });
|
||||
|
||||
req.user?.BookmarkedSongs.splice(idx, 1);
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
|
||||
})
|
||||
|
||||
App.get("/song/data/:InternalID", async (req, res) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "Song not found." });
|
||||
|
||||
res.json(SongData.Package());
|
||||
})
|
||||
|
||||
App.get("/available", (__, res) => res.json(Object.values(OriginalSparks!).filter(x => !!x.track).map(x => { return { Name: x.track.tt, Template: x.track.ti.substring(11) }; }).sort((a, b) => a.Name.toLowerCase() > b.Name.toLowerCase() ? 1 : -1)))
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/library"
|
||||
import { Router } from "express";
|
||||
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||
import { Song } from "../Schemas/Song";
|
||||
import { OriginalSparks } from "../Modules/FNUtil";
|
||||
import j from "joi";
|
||||
|
||||
const App = Router();
|
||||
|
||||
App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => {
|
||||
res.json({
|
||||
Bookmarks: req.user?.BookmarkedSongs.map(x => x.Package()),
|
||||
Library: req.user?.Library
|
||||
})
|
||||
})
|
||||
|
||||
App.post("/me/activate",
|
||||
RequireAuthentication(),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required(),
|
||||
ToOverride: j.string().pattern(/^sid_placeholder_(\d){1,3}$/i).required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
if (req.user!.Library!.length >= 15)
|
||||
return res.status(400).json({ errorMessage: "You have too many active songs. Please deactivate some to free up space." });
|
||||
|
||||
if (req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase() || x.Overriding.toLowerCase() === req.body.ToOverride.toLowerCase()) !== -1)
|
||||
return res.status(400).json({ errorMessage: "This song is already activated." });
|
||||
|
||||
if (!await Song.exists({ where: { ID: req.body.SongID } }))
|
||||
return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
|
||||
|
||||
req.user!.Library.push({ SongID: req.body.SongID.toLowerCase(), Overriding: req.body.ToOverride.toLowerCase() });
|
||||
req.user!.save();
|
||||
|
||||
res.json(req.user!.Library);
|
||||
})
|
||||
|
||||
App.post("/me/deactivate",
|
||||
RequireAuthentication(),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
const idx = req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase());
|
||||
if (idx === -1)
|
||||
return res.status(400).json({ errorMessage: "This song is not activated." });
|
||||
|
||||
req.user?.Library.splice(idx, 1);
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.Library);
|
||||
})
|
||||
|
||||
App.post("/me/bookmark",
|
||||
RequireAuthentication({ BookmarkedSongs: true }),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
if (req.user?.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()) !== -1)
|
||||
return res.status(400).json({ errorMessage: "You're already subscribed to this song." });
|
||||
|
||||
const SongData = await Song.findOne({ where: { ID: req.body.SongID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
|
||||
|
||||
req.user?.BookmarkedSongs.push(SongData);
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
|
||||
})
|
||||
|
||||
App.post("/me/unbookmark",
|
||||
RequireAuthentication(),
|
||||
ValidateBody(j.object({
|
||||
SongID: j.string().uuid().required()
|
||||
})),
|
||||
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 aren't subscribed to this song." });
|
||||
|
||||
req.user?.BookmarkedSongs.splice(idx, 1);
|
||||
req.user?.save();
|
||||
|
||||
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
|
||||
})
|
||||
|
||||
App.get("/song/data/:InternalID", async (req, res) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "Song not found." });
|
||||
|
||||
res.json(SongData.Package());
|
||||
})
|
||||
|
||||
App.get("/available", (__, res) => res.json(Object.values(OriginalSparks!).filter(x => !!x.track).map(x => { return { Name: x.track.tt, Template: x.track.ti.substring(11) }; }).sort((a, b) => a.Name.toLowerCase() > b.Name.toLowerCase() ? 1 : -1)))
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/library"
|
||||
}
|
|
@ -1,64 +1,64 @@
|
|||
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||
import { Router } from "express";
|
||||
import { Song } from "../Schemas/Song";
|
||||
import { Rating } from "../Schemas/Rating";
|
||||
import j from "joi";
|
||||
|
||||
const App = Router();
|
||||
|
||||
App.get("/:InternalID", async (req, res) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Ratings: true } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "The song you're trying to get the rating for has not been found." });
|
||||
|
||||
let Average = 0;
|
||||
if (SongData.Ratings.length > 0) {
|
||||
SongData.Ratings.map(x => Average += x.Stars);
|
||||
Average = Average / SongData.Ratings.length;
|
||||
}
|
||||
|
||||
res.json({
|
||||
Average,
|
||||
Amount: SongData.Ratings.length
|
||||
});
|
||||
})
|
||||
|
||||
App.post("/:InternalID",
|
||||
RequireAuthentication({ Ratings: { Rated: true } }),
|
||||
ValidateBody(j.object({
|
||||
Rating: j.number().integer().min(1).max(5).required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "The song you're trying to get the rating for has not been found." });
|
||||
|
||||
const Existing = req.user?.Ratings.find(x => SongData.ID === x.Rated.ID)
|
||||
if (Existing)
|
||||
{
|
||||
Existing.Stars = req.body.Rating as number;
|
||||
await Existing.save();
|
||||
return res.json({
|
||||
...Existing,
|
||||
Author: undefined,
|
||||
Rated: SongData.ID
|
||||
});
|
||||
}
|
||||
|
||||
const CreatedRating = await Rating.create({
|
||||
Author: req.user,
|
||||
Rated: SongData,
|
||||
Stars: req.body.Rating as number
|
||||
}).save();
|
||||
|
||||
res.json({
|
||||
...CreatedRating,
|
||||
Author: undefined,
|
||||
Rated: SongData.ID
|
||||
});
|
||||
})
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/ratings"
|
||||
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||
import { Router } from "express";
|
||||
import { Song } from "../Schemas/Song";
|
||||
import { Rating } from "../Schemas/Rating";
|
||||
import j from "joi";
|
||||
|
||||
const App = Router();
|
||||
|
||||
App.get("/:InternalID", async (req, res) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Ratings: true } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "The song you're trying to get the rating for has not been found." });
|
||||
|
||||
let Average = 0;
|
||||
if (SongData.Ratings.length > 0) {
|
||||
SongData.Ratings.map(x => Average += x.Stars);
|
||||
Average = Average / SongData.Ratings.length;
|
||||
}
|
||||
|
||||
res.json({
|
||||
Average,
|
||||
Amount: SongData.Ratings.length
|
||||
});
|
||||
})
|
||||
|
||||
App.post("/:InternalID",
|
||||
RequireAuthentication({ Ratings: { Rated: true } }),
|
||||
ValidateBody(j.object({
|
||||
Rating: j.number().integer().min(1).max(5).required()
|
||||
})),
|
||||
async (req, res) => {
|
||||
const SongData = await Song.findOne({ where: { ID: req.params.InternalID } });
|
||||
if (!SongData)
|
||||
return res.status(404).json({ errorMessage: "The song you're trying to get the rating for has not been found." });
|
||||
|
||||
const Existing = req.user?.Ratings.find(x => SongData.ID === x.Rated.ID)
|
||||
if (Existing)
|
||||
{
|
||||
Existing.Stars = req.body.Rating as number;
|
||||
await Existing.save();
|
||||
return res.json({
|
||||
...Existing,
|
||||
Author: undefined,
|
||||
Rated: SongData.ID
|
||||
});
|
||||
}
|
||||
|
||||
const CreatedRating = await Rating.create({
|
||||
Author: req.user,
|
||||
Rated: SongData,
|
||||
Stars: req.body.Rating as number
|
||||
}).save();
|
||||
|
||||
res.json({
|
||||
...CreatedRating,
|
||||
Author: undefined,
|
||||
Rated: SongData.ID
|
||||
});
|
||||
})
|
||||
|
||||
export default {
|
||||
App,
|
||||
DefaultAPI: "/api/ratings"
|
||||
}
|
|
@ -1,21 +1,21 @@
|
|||
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { Song } from "./Song";
|
||||
|
||||
@Entity()
|
||||
export class ForcedCategory extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
ID: string;
|
||||
|
||||
@Column()
|
||||
Header: string;
|
||||
|
||||
@Column()
|
||||
Activated: boolean;
|
||||
|
||||
@Column()
|
||||
Priority: number;
|
||||
|
||||
@ManyToMany(() => Song, { eager: true })
|
||||
@JoinTable()
|
||||
Songs: Song[];
|
||||
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { Song } from "./Song";
|
||||
|
||||
@Entity()
|
||||
export class ForcedCategory extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
ID: string;
|
||||
|
||||
@Column()
|
||||
Header: string;
|
||||
|
||||
@Column()
|
||||
Activated: boolean;
|
||||
|
||||
@Column()
|
||||
Priority: number;
|
||||
|
||||
@ManyToMany(() => Song, { eager: true })
|
||||
@JoinTable()
|
||||
Songs: Song[];
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { User } from "./User";
|
||||
import { Song } from "./Song";
|
||||
|
||||
@Entity()
|
||||
export class Rating extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
ID: string;
|
||||
|
||||
@ManyToOne(() => User, U => U.Ratings)
|
||||
Author: User;
|
||||
|
||||
@ManyToOne(() => Song, S => S.Ratings)
|
||||
Rated: Song;
|
||||
|
||||
@Column()
|
||||
Stars: number;
|
||||
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { User } from "./User";
|
||||
import { Song } from "./Song";
|
||||
|
||||
@Entity()
|
||||
export class Rating extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
ID: string;
|
||||
|
||||
@ManyToOne(() => User, U => U.Ratings)
|
||||
Author: User;
|
||||
|
||||
@ManyToOne(() => Song, S => S.Ratings)
|
||||
Rated: Song;
|
||||
|
||||
@Column()
|
||||
Stars: number;
|
||||
}
|
|
@ -1,29 +1,29 @@
|
|||
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||
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()
|
||||
ID: string;
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
Library: { SongID: string, Overriding: string }[];
|
||||
|
||||
@Column({ default: UserPermissions.User })
|
||||
PermissionLevel: UserPermissions;
|
||||
|
||||
@OneToMany(() => Rating, R => R.Author)
|
||||
Ratings: Rating[];
|
||||
|
||||
@ManyToMany(() => Song, { eager: true })
|
||||
@JoinTable()
|
||||
BookmarkedSongs: Song[];
|
||||
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||
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()
|
||||
ID: string;
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
Library: { SongID: string, Overriding: string }[];
|
||||
|
||||
@Column({ default: UserPermissions.User })
|
||||
PermissionLevel: UserPermissions;
|
||||
|
||||
@OneToMany(() => Rating, R => R.Author)
|
||||
Ratings: Rating[];
|
||||
|
||||
@ManyToMany(() => Song, { eager: true })
|
||||
@JoinTable()
|
||||
BookmarkedSongs: Song[];
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
import "./Handlers/Database";
|
||||
import "./Handlers/Server";
|
||||
|
||||
/*
|
||||
Welcome to Mc's BasedServer template! (v1.1 - 30.12.2023 update)
|
||||
This is the exact same base I use for projects like Dispriv, Birdnest, GDTS and more.
|
||||
|
||||
Here's a quick overview:
|
||||
- Handlers:
|
||||
Handlers are the big boys - websockets, servers, anything that has to be constantly running.
|
||||
Personally, I always import those here in index, like the Server handler.
|
||||
- Modules:
|
||||
These are smaller, util classes. A good example of that is the Logger util, which lets you
|
||||
print to the console with COLORS!
|
||||
- Routes:
|
||||
The routes folder houses scripts with a router, some endpoints and other fun stuff!
|
||||
|
||||
I recommend familiarizing yourself with all the code before adding anything yourself.
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
import "./Handlers/Database";
|
||||
import "./Handlers/Server";
|
||||
|
||||
/*
|
||||
Welcome to Mc's BasedServer template! (v1.1 - 30.12.2023 update)
|
||||
This is the exact same base I use for projects like Dispriv, Birdnest, GDTS and more.
|
||||
|
||||
Here's a quick overview:
|
||||
- Handlers:
|
||||
Handlers are the big boys - websockets, servers, anything that has to be constantly running.
|
||||
Personally, I always import those here in index, like the Server handler.
|
||||
- Modules:
|
||||
These are smaller, util classes. A good example of that is the Logger util, which lets you
|
||||
print to the console with COLORS!
|
||||
- Routes:
|
||||
The routes folder houses scripts with a router, some endpoints and other fun stuff!
|
||||
|
||||
I recommend familiarizing yourself with all the code before adding anything yourself.
|
||||
*/
|
|
@ -30,12 +30,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@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"
|
||||
|
|
|
@ -1,107 +1,107 @@
|
|||
{
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["./Source"],
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
/* Projects */
|
||||
"incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */,
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
|
||||
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "CommonJS" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
/* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["node"] /* Specify type package names to be included without being referenced in a source file. */,
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
"resolveJsonModule": true /* Enable importing .json files. */,
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
|
||||
"checkJs": true /* Enable error reporting in type-checked JavaScript files. */,
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": false /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||
"declarationMap": false /* Create sourcemaps for d.ts files. */,
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./bin/" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
"importHelpers": true /* Allow importing helper functions from tslib once per project, instead of including them per-file. */,
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
"noEmitHelpers": true /* Disable e custom helper functions like '__extends' in compiled output. */,
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
|
||||
"strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
"strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */,
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"noErrorTruncation": true /* Disable truncating types in error messages. */
|
||||
},
|
||||
"ts-node": {
|
||||
"files": true
|
||||
}
|
||||
}
|
||||
{
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["./Source"],
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
/* Projects */
|
||||
"incremental": true /* Save .tsbuildinfo files to allow for incremental compilation of projects. */,
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
|
||||
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "CommonJS" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
/* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["node"] /* Specify type package names to be included without being referenced in a source file. */,
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
"resolveJsonModule": true /* Enable importing .json files. */,
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
|
||||
"checkJs": true /* Enable error reporting in type-checked JavaScript files. */,
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": false /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
|
||||
"declarationMap": false /* Create sourcemaps for d.ts files. */,
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "../Out/bin/" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
"importHelpers": true /* Allow importing helper functions from tslib once per project, instead of including them per-file. */,
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
"noEmitHelpers": true /* Disable e custom helper functions like '__extends' in compiled output. */,
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
|
||||
"strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
"strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */,
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
"alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"noErrorTruncation": true /* Disable truncating types in error messages. */
|
||||
},
|
||||
"ts-node": {
|
||||
"files": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.webp" />
|
||||
<link rel="icon" type="image/png" href="/public/favicon.webp" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
|
@ -11,10 +11,10 @@
|
|||
rel="stylesheet">
|
||||
<link href="https://unpkg.com/@primer/css/dist/primer.css" rel="stylesheet" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta content="goku doing party party emote fortnite" property="og:title" />
|
||||
<meta content="party party party party" property="og:description" />
|
||||
<meta content="Partypack - #1 Festival Modding Tool" property="og:title" />
|
||||
<meta content="Join hundreds of users in Partypack - the one and only open-source Fortnite Festival modding utility available now!" property="og:description" />
|
||||
<meta content="https://partypack.mcthe.dev" property="og:url" />
|
||||
<meta content="#FFC300" data-react-helmet="true" name="theme-color" />
|
||||
<meta content="#40A0ED" data-react-helmet="true" name="theme-color" />
|
||||
<title>Partypack</title>
|
||||
</head>
|
||||
|
||||
|
|
10280
package-lock.json
generated
10280
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
100
package.json
100
package.json
|
@ -1,47 +1,53 @@
|
|||
{
|
||||
"name": "snippets",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"create": "mkdir ./Out && npm run build && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env ./Out && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
||||
"publish": "npm run create && ssh shady \"cd /home/PartypackProd; rm -rf ./Out\" && scp -r ./Out shady:/home/PartypackProd && ssh shady \"cd /home/PartypackProd/Out; npm i; pm2 restart PartypackProd --update-env\" && rm -rf ./Out",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"dev:all": "start cmd.exe /k \"cd ./Server && npm run dev:watch\" && vite"
|
||||
},
|
||||
"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",
|
||||
"exif-reader": "^2.0.0",
|
||||
"joi": "^17.12.0",
|
||||
"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": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"sass": "^1.69.7",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "partypack",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:prod": "vite build",
|
||||
"build:stage": "vite build --mode staging",
|
||||
|
||||
"create:prod": "mkdir ./Out && npm run build:prod && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.prod ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
||||
"publish:prod": "npm run create:prod && ssh partypack \"cd /home/PartypackProd; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackProd && ssh partypack \"cd /home/PartypackProd/Out && npm i && pm2 restart PartypackProd --update-env\" && rm -rf ./Out",
|
||||
|
||||
"create:stage": "mkdir ./Out && npm run build:stage && mv ./dist ./Out/dist && cd Server && tsc && cd .. && cp ./Server/.env.staging ./Out/.env && cp ./Server/package.json ./Out && cp ./Server/package-lock.json ./Out",
|
||||
"publish:stage": "npm run create:stage && ssh partypack \"cd /home/PartypackStage; rm -rf ./Out\" && scp -r ./Out partypack:/home/PartypackStage && ssh partypack \"cd /home/PartypackStage/Out && npm i && pm2 restart PartypackStage --update-env\" && rm -rf ./Out",
|
||||
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"dev:all": "start cmd.exe /k \"cd ./Server && npm run dev:watch\" && vite"
|
||||
},
|
||||
"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",
|
||||
"exif-reader": "^2.0.0",
|
||||
"joi": "^17.12.0",
|
||||
"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": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"sass": "^1.69.7",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +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>
|
||||
)
|
||||
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, onEdit, onDelete, priority }: { categoryName: string, songs: any[], isForced: boolean, moveUp: () => void, moveDown: () => void, onEdit: () => 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" onClick={onEdit} />
|
||||
<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>
|
||||
)
|
||||
}
|
|
@ -1,51 +1,51 @@
|
|||
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, 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";
|
||||
|
||||
export function SiteHeader() {
|
||||
const {state, setState} = useContext(SiteContext);
|
||||
const [cookies] = useCookies();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Data = await axios.get("/api/discord/url");
|
||||
const Details: UserDetailInterface = cookies["UserDetails"] ? JSON.parse(decodeURI(Buffer.from(cookies["UserDetails"], "hex").toString())) : null;
|
||||
|
||||
setState({
|
||||
...state,
|
||||
UserDetails: Details,
|
||||
DiscordOauthURL: Data.data
|
||||
});
|
||||
})();
|
||||
}, [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 full sx={{ cursor: "pointer" }} onClick={() => navigate("/rotation")}>Daily Rotation</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/creators")}>Top Creators</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tracks")}>Tracks</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tutorials")}>Tutorials</Header.Item>
|
||||
<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>
|
||||
{ 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> :
|
||||
<Box sx={{ cursor: "pointer" }} onClick={() => state.DiscordOauthURL ? window.location.assign(state.DiscordOauthURL) : toast("Cannot redirect to login. No Discord OAuth URL has been received from the backend.", { type: "error" })}><SignInIcon size={16} /></Box>
|
||||
}
|
||||
</Header>
|
||||
);
|
||||
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, 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";
|
||||
|
||||
export function SiteHeader() {
|
||||
const {state, setState} = useContext(SiteContext);
|
||||
const [cookies] = useCookies();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Data = await axios.get("/api/discord/url");
|
||||
const Details: UserDetailInterface = cookies["UserDetails"] ? JSON.parse(decodeURI(Buffer.from(cookies["UserDetails"], "hex").toString())) : null;
|
||||
|
||||
setState({
|
||||
...state,
|
||||
UserDetails: Details,
|
||||
DiscordOauthURL: Data.data
|
||||
});
|
||||
})();
|
||||
}, [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 full sx={{ cursor: "pointer" }} onClick={() => navigate("/rotation")}>Daily Rotation</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/creators")}>Top Creators</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tracks")}>Tracks</Header.Item>
|
||||
<Header.Item full sx={{ cursor: "pointer" }} onClick={() => navigate("/tutorials")}>Tutorials</Header.Item>
|
||||
<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>
|
||||
{ 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> :
|
||||
<Box sx={{ cursor: "pointer" }} onClick={() => state.DiscordOauthURL ? window.location.assign(state.DiscordOauthURL) : toast("Cannot redirect to login. No Discord OAuth URL has been received from the backend.", { type: "error" })}><SignInIcon size={16} /></Box>
|
||||
}
|
||||
</Header>
|
||||
);
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
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 {state} = useContext(SiteContext);
|
||||
|
||||
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;
|
||||
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 {state} = useContext(SiteContext);
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
.content {
|
||||
padding: 10px;
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.songCategory {
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#__primerPortalRoot__ > div {
|
||||
z-index: 1000 !important;
|
||||
.content {
|
||||
padding: 10px;
|
||||
margin-left: 15%;
|
||||
margin-right: 15%;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.songCategory {
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#__primerPortalRoot__ > div {
|
||||
z-index: 1000 !important;
|
||||
}
|
|
@ -1,13 +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>
|
||||
)
|
||||
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>
|
||||
)
|
||||
}
|
|
@ -1,168 +1,168 @@
|
|||
import { ActionList, ActionMenu, FormControl, TextInput, Heading, Button } from "@primer/react";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Buffer } from "buffer/";
|
||||
import axios from "axios";
|
||||
|
||||
const style = { paddingTop: 3 };
|
||||
|
||||
export function AdminCreateTrack() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [Key, setKey] = useState<string>("Select a key...");
|
||||
const [Scale, setScale] = useState<string>("Select a scale...");
|
||||
const [GuitarStarterType, setGuitarStarterType] = useState<string>("Select the starter type...");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>Create a New Track</Heading>
|
||||
<form method="GET" action="" ref={formRef}>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Song Name</FormControl.Label>
|
||||
<TextInput />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Artist</FormControl.Label>
|
||||
<FormControl.Caption>If there are multiple artists, separate them with a comma.</FormControl.Caption>
|
||||
<TextInput />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Album</FormControl.Label>
|
||||
<TextInput />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Release Year</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Length (in seconds)</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Key</FormControl.Label>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>{Key}</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
{
|
||||
["A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb"].map(x => {
|
||||
return (
|
||||
<ActionList.Item onSelect={() => setKey(x)}>
|
||||
{x}
|
||||
</ActionList.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Scale</FormControl.Label>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>{Scale}</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
<ActionList.Item onSelect={() => setScale("Minor")}>Minor</ActionList.Item>
|
||||
<ActionList.Item onSelect={() => setScale("Major")}>Major</ActionList.Item>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Lead Type</FormControl.Label>
|
||||
<FormControl.Caption>This is defining what lead instrument the song is going to start with. You can change the instrument mid-game with [keytar] and [guitar] text events.</FormControl.Caption>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>{GuitarStarterType}</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
<ActionList.Item onSelect={() => setGuitarStarterType("Guitar")}>Guitar</ActionList.Item>
|
||||
<ActionList.Item onSelect={() => setGuitarStarterType("Keytar")}>Keytar</ActionList.Item>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Tempo</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>MIDI File</FormControl.Label>
|
||||
<TextInput type="file" />
|
||||
<FormControl.Caption>You can use the #tools-and-resources channel to find useful resources on how to create MIDIs.</FormControl.Caption>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Cover Image</FormControl.Label>
|
||||
<TextInput type="file" />
|
||||
<FormControl.Caption>Must be a 1:1 ratio. Max: 2048x2048, min: 512x512</FormControl.Caption>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Lead Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Drums Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Vocals Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={style}>
|
||||
<FormControl.Label>Bass Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<Button sx={{ marginTop: 2 }} type="submit" onClick={async e => {
|
||||
e.preventDefault();
|
||||
console.log(formRef);
|
||||
|
||||
if (formRef.current == null)
|
||||
return;
|
||||
|
||||
const Name = (formRef.current[0] as HTMLInputElement).value;
|
||||
const ArtistName = (formRef.current[1] as HTMLInputElement).value;
|
||||
const Album = (formRef.current[2] as HTMLInputElement).value;
|
||||
const Year = (formRef.current[3] as HTMLInputElement).value;
|
||||
const Length = (formRef.current[4] as HTMLInputElement).valueAsNumber;
|
||||
//const Key = (formRef.current[5] as HTMLInputElement).value;
|
||||
//const Scale = (formRef.current[6] as HTMLInputElement).value;
|
||||
//const GuitarStarterType = (formRef.current[7] as HTMLInputElement).value;
|
||||
const Tempo = (formRef.current[8] as HTMLInputElement).valueAsNumber;
|
||||
const Midi = (formRef.current[9] as HTMLInputElement).files![0];
|
||||
const Cover = (formRef.current[10] as HTMLInputElement).files![0];
|
||||
const GuitarDifficulty = (formRef.current[11] as HTMLInputElement).valueAsNumber;
|
||||
const DrumsDifficulty = (formRef.current[12] as HTMLInputElement).valueAsNumber;
|
||||
const VocalsDifficulty = (formRef.current[13] as HTMLInputElement).valueAsNumber;
|
||||
const BassDifficulty = (formRef.current[14] as HTMLInputElement).valueAsNumber;
|
||||
|
||||
const SongData = await axios.post("/admin/api/create/song", {
|
||||
Name,
|
||||
ArtistName,
|
||||
Album,
|
||||
Year,
|
||||
Length,
|
||||
Key,
|
||||
Scale,
|
||||
GuitarStarterType,
|
||||
Tempo,
|
||||
GuitarDifficulty,
|
||||
DrumsDifficulty,
|
||||
VocalsDifficulty,
|
||||
BassDifficulty
|
||||
});
|
||||
|
||||
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
|
||||
|
||||
if (SongData.status !== 200)
|
||||
return;
|
||||
|
||||
const MidiRes = await axios.post("/admin/api/upload/midi", {
|
||||
Data: Buffer.from(
|
||||
await Midi.arrayBuffer()
|
||||
).toString("hex"),
|
||||
TargetSong: SongData.data.ID
|
||||
});
|
||||
toast(MidiRes.data, { type: MidiRes.status === 200 ? "success" : "error" });
|
||||
|
||||
const CoverRes = await axios.post("/admin/api/upload/cover", { Data: Buffer.from(await Cover.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
|
||||
toast(CoverRes.data, { type: CoverRes.status === 200 ? "success" : "error" });
|
||||
}}>Create</Button>
|
||||
|
||||
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
import { ActionList, ActionMenu, FormControl, TextInput, Heading, Button } from "@primer/react";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Buffer } from "buffer/";
|
||||
import axios from "axios";
|
||||
|
||||
const formControlStyle = { paddingTop: 3 };
|
||||
|
||||
export function AdminCreateTrack() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [Key, setKey] = useState<string>("Select a key...");
|
||||
const [Scale, setScale] = useState<string>("Select a scale...");
|
||||
const [GuitarStarterType, setGuitarStarterType] = useState<string>("Select the starter type...");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>Create a New Track</Heading>
|
||||
<form method="GET" action="" ref={formRef}>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Song Name</FormControl.Label>
|
||||
<TextInput />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Artist</FormControl.Label>
|
||||
<FormControl.Caption>If there are multiple artists, separate them with a comma.</FormControl.Caption>
|
||||
<TextInput />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Album</FormControl.Label>
|
||||
<TextInput />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Release Year</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Length (in seconds)</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Key</FormControl.Label>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>{Key}</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
{
|
||||
["A", "Ab", "B", "Bb", "C", "Cb", "D", "Db", "E", "Eb", "F", "Fb", "G", "Gb"].map(x => {
|
||||
return (
|
||||
<ActionList.Item onSelect={() => setKey(x)}>
|
||||
{x}
|
||||
</ActionList.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Scale</FormControl.Label>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>{Scale}</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
<ActionList.Item onSelect={() => setScale("Minor")}>Minor</ActionList.Item>
|
||||
<ActionList.Item onSelect={() => setScale("Major")}>Major</ActionList.Item>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Lead Type</FormControl.Label>
|
||||
<FormControl.Caption>This is defining what lead instrument the song is going to start with. You can change the instrument mid-game with [keytar] and [guitar] text events.</FormControl.Caption>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>{GuitarStarterType}</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
<ActionList.Item onSelect={() => setGuitarStarterType("Guitar")}>Guitar</ActionList.Item>
|
||||
<ActionList.Item onSelect={() => setGuitarStarterType("Keytar")}>Keytar</ActionList.Item>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Tempo</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>MIDI File</FormControl.Label>
|
||||
<TextInput type="file" />
|
||||
<FormControl.Caption>You can use the #tools-and-resources channel to find useful resources on how to create MIDIs.</FormControl.Caption>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Cover Image</FormControl.Label>
|
||||
<TextInput type="file" />
|
||||
<FormControl.Caption>Must be a 1:1 ratio. Max: 2048x2048, min: 512x512</FormControl.Caption>
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Lead Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Drums Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Vocals Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Bass Difficulty</FormControl.Label>
|
||||
<TextInput type="number" />
|
||||
</FormControl>
|
||||
<Button sx={{ marginTop: 2 }} type="submit" onClick={async e => {
|
||||
e.preventDefault();
|
||||
console.log(formRef);
|
||||
|
||||
if (formRef.current == null)
|
||||
return;
|
||||
|
||||
const Name = (formRef.current[0] as HTMLInputElement).value;
|
||||
const ArtistName = (formRef.current[1] as HTMLInputElement).value;
|
||||
const Album = (formRef.current[2] as HTMLInputElement).value;
|
||||
const Year = (formRef.current[3] as HTMLInputElement).value;
|
||||
const Length = (formRef.current[4] as HTMLInputElement).valueAsNumber;
|
||||
//const Key = (formRef.current[5] as HTMLInputElement).value;
|
||||
//const Scale = (formRef.current[6] as HTMLInputElement).value;
|
||||
//const GuitarStarterType = (formRef.current[7] as HTMLInputElement).value;
|
||||
const Tempo = (formRef.current[8] as HTMLInputElement).valueAsNumber;
|
||||
const Midi = (formRef.current[9] as HTMLInputElement).files![0];
|
||||
const Cover = (formRef.current[10] as HTMLInputElement).files![0];
|
||||
const GuitarDifficulty = (formRef.current[11] as HTMLInputElement).valueAsNumber;
|
||||
const DrumsDifficulty = (formRef.current[12] as HTMLInputElement).valueAsNumber;
|
||||
const VocalsDifficulty = (formRef.current[13] as HTMLInputElement).valueAsNumber;
|
||||
const BassDifficulty = (formRef.current[14] as HTMLInputElement).valueAsNumber;
|
||||
|
||||
const SongData = await axios.post("/api/admin/create/song", {
|
||||
Name,
|
||||
ArtistName,
|
||||
Album,
|
||||
Year,
|
||||
Length,
|
||||
Key,
|
||||
Scale,
|
||||
GuitarStarterType,
|
||||
Tempo,
|
||||
GuitarDifficulty,
|
||||
DrumsDifficulty,
|
||||
VocalsDifficulty,
|
||||
BassDifficulty
|
||||
});
|
||||
|
||||
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
|
||||
|
||||
if (SongData.status !== 200)
|
||||
return;
|
||||
|
||||
const MidiRes = await axios.post("/api/admin/upload/midi", {
|
||||
Data: Buffer.from(
|
||||
await Midi.arrayBuffer()
|
||||
).toString("hex"),
|
||||
TargetSong: SongData.data.ID
|
||||
});
|
||||
toast(MidiRes.data, { type: MidiRes.status === 200 ? "success" : "error" });
|
||||
|
||||
const CoverRes = await axios.post("/api/admin/upload/cover", { Data: Buffer.from(await Cover.arrayBuffer()).toString("hex"), TargetSong: SongData.data.ID });
|
||||
toast(CoverRes.data, { type: CoverRes.status === 200 ? "success" : "error" });
|
||||
}}>Create</Button>
|
||||
|
||||
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,86 +1,161 @@
|
|||
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>
|
||||
</>
|
||||
)
|
||||
import { Box, Button, Dialog, FormControl, Heading, TextInput } from "@primer/react";
|
||||
import { AdminCategory } from "../components/AdminCategory";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { moveElement } from "../utils/Extensions";
|
||||
import { toast } from "react-toastify";
|
||||
import axios from "axios";
|
||||
|
||||
export interface LibraryObject {
|
||||
ID: string,
|
||||
Header: string,
|
||||
Songs: unknown[],
|
||||
Custom: boolean,
|
||||
Priority: number,
|
||||
ShouldDelete: boolean
|
||||
}
|
||||
|
||||
const formControlStyle = { paddingBottom: 3 };
|
||||
|
||||
export function AdminFeaturedTab() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [library, setLibrary] = useState<LibraryObject[] | null>(null);
|
||||
const [hackyRevertChanges, setHackyRevertChanges] = useState<boolean>(false); // trust this
|
||||
const [addSongsOpen, setAddSongsOpen] = useState<boolean>(false);
|
||||
const [editedCategory, setEditedCategory] = useState<{ Obj: LibraryObject, Index: number }>(null)
|
||||
|
||||
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.map(x => {
|
||||
return {
|
||||
...x,
|
||||
ShouldDelete: false
|
||||
};
|
||||
}));
|
||||
})();
|
||||
}, [hackyRevertChanges]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog isOpen={addSongsOpen} onDismiss={() => setAddSongsOpen(false)}>
|
||||
<Dialog.Header>Modify Category</Dialog.Header>
|
||||
<Box p={3}>
|
||||
<form method="GET" action="" ref={formRef}>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Category Name</FormControl.Label>
|
||||
<TextInput placeholder={editedCategory?.Obj.Header} />
|
||||
</FormControl>
|
||||
<FormControl required={true} sx={formControlStyle}>
|
||||
<FormControl.Label>Custom Priority</FormControl.Label>
|
||||
<TextInput placeholder={editedCategory?.Obj.Priority.toString()} type="number" />
|
||||
<FormControl.Caption>The lower, the higher the category goes.</FormControl.Caption>
|
||||
</FormControl>
|
||||
</form>
|
||||
<Box sx={{ float: "right", display: "inline-flex", gap: 2, marginBottom: 3 }}>
|
||||
<Button variant="primary" type="submit" onClick={async e => {
|
||||
e.preventDefault();
|
||||
setAddSongsOpen(false);
|
||||
|
||||
const Name = (formRef.current[0] as HTMLInputElement).value;
|
||||
const Priority = (formRef.current[1] as HTMLInputElement).valueAsNumber;
|
||||
|
||||
library[editedCategory.Index] = {
|
||||
...editedCategory.Obj,
|
||||
Header: Name.trim() === "" ? editedCategory.Obj.Header : Name,
|
||||
Priority: isNaN(Priority) ? editedCategory.Obj.Priority : Priority
|
||||
};
|
||||
setLibrary(library.sort((a, b) => a.Priority - b.Priority));
|
||||
}}>Confirm changes</Button>
|
||||
<Button onClick={() => setAddSongsOpen(false)}>Cancel</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>
|
||||
<Heading>Featured Tabs</Heading>
|
||||
{
|
||||
library?.filter(x => !x.ShouldDelete).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={() => {
|
||||
library[library.findIndex(y => y.ID === x.ID)].ShouldDelete = true;
|
||||
setLibrary([...library])
|
||||
}}
|
||||
onEdit={() => {
|
||||
setEditedCategory({ Obj: x, Index: i });
|
||||
setAddSongsOpen(true);
|
||||
}} />
|
||||
)
|
||||
})
|
||||
}
|
||||
<Box sx={{ float: "right", display: "inline-flex", gap: 2 }}>
|
||||
<Button variant="primary" onClick={async () => {
|
||||
console.log(library);
|
||||
const SaveResponse = await axios.post("/api/admin/update/discovery", [
|
||||
...library!.filter(x => x.Custom).map(
|
||||
x => {
|
||||
return {
|
||||
...x,
|
||||
Custom: undefined,
|
||||
Activated: undefined,
|
||||
Songs: x.Songs.map(y => y.ID)
|
||||
};
|
||||
}
|
||||
)
|
||||
]);
|
||||
|
||||
if (SaveResponse.status === 200)
|
||||
return toast("Saved changes successfully.", { type: "success" });
|
||||
|
||||
toast("An unknown error has occured. Please check the console.", { type: "error" });
|
||||
}}>Save</Button>
|
||||
<Button onClick={() => setHackyRevertChanges(!hackyRevertChanges)}>Revert changes</Button>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
import { Box, Button, Text } from "@primer/react";
|
||||
import { PageHeader } from "@primer/react/drafts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function AdminHome() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PageHeader>
|
||||
<PageHeader.TitleArea>
|
||||
<PageHeader.Title>Partypack Admin Management Panel</PageHeader.Title>
|
||||
</PageHeader.TitleArea>
|
||||
<PageHeader.Description>
|
||||
TEMP
|
||||
<Button onClick={() => navigate("/admin/tracks")}>Tracks</Button>
|
||||
<Button onClick={() => navigate("/admin/featured")}>Featured Tab Management</Button>
|
||||
</PageHeader.Description>
|
||||
</PageHeader>
|
||||
</Box>
|
||||
)
|
||||
import { Box, Button, Text } from "@primer/react";
|
||||
import { PageHeader } from "@primer/react/drafts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function AdminHome() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<PageHeader>
|
||||
<PageHeader.TitleArea>
|
||||
<PageHeader.Title>Partypack Admin Management Panel</PageHeader.Title>
|
||||
</PageHeader.TitleArea>
|
||||
<PageHeader.Description>
|
||||
TEMP
|
||||
<Button onClick={() => navigate("/admin/tracks")}>Tracks</Button>
|
||||
<Button onClick={() => navigate("/admin/featured")}>Featured Tab Management</Button>
|
||||
</PageHeader.Description>
|
||||
</PageHeader>
|
||||
</Box>
|
||||
)
|
||||
}
|
|
@ -1,37 +1,37 @@
|
|||
import axios from "axios";
|
||||
import { Box, Button, Heading } from "@primer/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Song } from "../components/Song";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function AdminTrackList() {
|
||||
const [tracks, setTracks] = useState<unknown[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Tracks = await axios.get("/admin/api/tracks");
|
||||
if (Tracks.status !== 200)
|
||||
return toast("Error while requesting tracks!");
|
||||
|
||||
setTracks(Tracks.data);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }} onClick={() => navigate("/admin/tracks/create")}>Create</Button></Heading>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
tracks.map(x => {
|
||||
return <Song data={x}>
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }}>View Details</Button>
|
||||
<Button sx={{ width: "100%" }} variant="danger">Disable</Button>
|
||||
</Song>
|
||||
})
|
||||
}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
import axios from "axios";
|
||||
import { Box, Button, Heading } from "@primer/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Song } from "../components/Song";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function AdminTrackList() {
|
||||
const [tracks, setTracks] = useState<unknown[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Tracks = await axios.get("/api/admin/tracks");
|
||||
if (Tracks.status !== 200)
|
||||
return toast("Error while requesting tracks!");
|
||||
|
||||
setTracks(Tracks.data);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }} onClick={() => navigate("/admin/tracks/create")}>Create</Button></Heading>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
tracks.map(x => {
|
||||
return <Song data={x}>
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }}>View Details</Button>
|
||||
<Button sx={{ width: "100%" }} variant="danger">Disable</Button>
|
||||
</Song>
|
||||
})
|
||||
}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { Box } from "@primer/react";
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
import { Box } from "@primer/react";
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,136 +1,136 @@
|
|||
import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, Heading, Text } from "@primer/react"
|
||||
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
|
||||
import { PageHeader } from "@primer/react/drafts";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { SiteContext } from "../utils/State";
|
||||
import { useCookies } from "react-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Song } from "../components/Song";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export function Profile() {
|
||||
const { state, setState } = useContext(SiteContext);
|
||||
const [, , removeCookie] = useCookies();
|
||||
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState<boolean>(false);
|
||||
const [librarySongs, setLibrarySongs] = useState<unknown[]>([]);
|
||||
const [bookmarkedSongs, setBookmarkedSongs] = useState<unknown[]>([]);
|
||||
const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]);
|
||||
const [overriding, setOverriding] = useState<unknown>({});
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Data = await axios.get("/api/library/me");
|
||||
const Overrides = await axios.get("/api/library/available");
|
||||
|
||||
if (Data.status !== 200 || Overrides.status !== 200)
|
||||
return toast("An error has occured while getting your library!", { type: "error" });
|
||||
|
||||
const LibSongs = (await Promise.all(Data.data.Library.map((x: { SongID: string; }) => axios.get(`/api/library/song/data/${x.SongID}`)))).map(x => { return { ...x.data, Override: Data.data.Library.find((y: { SongID: string; }) => y.SongID === x.data.ID).Overriding } });
|
||||
const BookSongs = (await Promise.all(Data.data.Bookmarks.map((x: { ID: string; }) => axios.get(`/api/library/song/data/${x.ID}`)))).map(x => x.data);
|
||||
setLibrarySongs(LibSongs);
|
||||
setBookmarkedSongs(BookSongs);
|
||||
setAvailableOverrides(Overrides.data);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
state.UserDetails ?
|
||||
<Box>
|
||||
<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 }); window.location.assign("/") }}>Log out</Button>
|
||||
</PageHeader.Actions>
|
||||
</PageHeader.TitleArea>
|
||||
</PageHeader>
|
||||
<Divider />
|
||||
<Heading sx={{ marginBottom: 2 }}>Active Songs</Heading>
|
||||
<Dialog isOpen={isActivateDialogOpen} onDismiss={() => setIsActivateDialogOpen(false)} aria-labelledby="header">
|
||||
<Dialog.Header id="header">Activate song</Dialog.Header>
|
||||
<Box p={3}>
|
||||
<Text>In order to activate a song for use, you need to sacrifice another song.<br />Please select a song you own to replace:</Text>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>Select a song...</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
<ActionList>
|
||||
{
|
||||
availableOverrides.map(x => {
|
||||
return <ActionList.Item onSelect={async () => {
|
||||
setIsActivateDialogOpen(false);
|
||||
const Res = await axios.post("/api/library/me/activate", { SongID: overriding!.ID!, ToOverride: x.Template });
|
||||
if (Res.status === 200)
|
||||
setLibrarySongs([
|
||||
...librarySongs,
|
||||
{ ...overriding!, Override: x.Template }
|
||||
])
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>
|
||||
{x.Name}
|
||||
</ActionList.Item>;
|
||||
})
|
||||
}
|
||||
</ActionList>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</Box>
|
||||
</Dialog>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
librarySongs.length >= 1 ?
|
||||
librarySongs.map(x => {
|
||||
return <Song data={x}>
|
||||
<center>Overriding: <Text sx={{ display: "block", fontWeight: "700", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{availableOverrides.find(y => y.Template.toLowerCase() === x.Override.toLowerCase())?.Name}</Text></center>
|
||||
<Button sx={{ width: "100%", marginTop: 2 }} variant="danger" onClick={async () => {
|
||||
const Res = await axios.post("/api/library/me/deactivate", { SongID: x.ID });
|
||||
if (Res.status === 200) {
|
||||
librarySongs.splice(librarySongs.findIndex(y => y.ID === x.ID), 1);
|
||||
setLibrarySongs([...librarySongs]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Remove from Active</Button>
|
||||
</Song>;
|
||||
})
|
||||
: <Text>You have no activated songs.</Text>
|
||||
}
|
||||
</Box>
|
||||
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>My Subscriptions</Heading>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
bookmarkedSongs.length >= 1 ?
|
||||
bookmarkedSongs.map(x => {
|
||||
return <Song data={x}>
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={() => { setIsActivateDialogOpen(true); setOverriding(x) }} disabled={librarySongs.findIndex(y => y.ID === x.ID) !== -1}>Add to Active</Button>
|
||||
<Button sx={{ width: "100%" }} variant="danger" onClick={async () => {
|
||||
const Res = await axios.post("/api/library/me/unbookmark", { SongID: x.ID });
|
||||
if (Res.status === 200) {
|
||||
bookmarkedSongs.splice(bookmarkedSongs.findIndex(y => y.ID === x.ID), 1);
|
||||
setBookmarkedSongs([...bookmarkedSongs]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Unsubscribe</Button>
|
||||
</Song>;
|
||||
})
|
||||
: <Text>You have no bookmarked songs.</Text>
|
||||
}
|
||||
</Box>
|
||||
</Box> :
|
||||
<>
|
||||
<Text>You are not logged in.<br />Log in using the button in the top right.</Text>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, Heading, Text } from "@primer/react"
|
||||
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
|
||||
import { PageHeader } from "@primer/react/drafts";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { SiteContext } from "../utils/State";
|
||||
import { useCookies } from "react-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Song } from "../components/Song";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export function Profile() {
|
||||
const { state, setState } = useContext(SiteContext);
|
||||
const [, , removeCookie] = useCookies();
|
||||
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState<boolean>(false);
|
||||
const [librarySongs, setLibrarySongs] = useState<unknown[]>([]);
|
||||
const [bookmarkedSongs, setBookmarkedSongs] = useState<unknown[]>([]);
|
||||
const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]);
|
||||
const [overriding, setOverriding] = useState<unknown>({});
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Data = await axios.get("/api/library/me");
|
||||
const Overrides = await axios.get("/api/library/available");
|
||||
|
||||
if (Data.status !== 200 || Overrides.status !== 200)
|
||||
return toast("An error has occured while getting your library!", { type: "error" });
|
||||
|
||||
const LibSongs = (await Promise.all(Data.data.Library.map((x: { SongID: string; }) => axios.get(`/api/library/song/data/${x.SongID}`)))).map(x => { return { ...x.data, Override: Data.data.Library.find((y: { SongID: string; }) => y.SongID === x.data.ID).Overriding } });
|
||||
const BookSongs = (await Promise.all(Data.data.Bookmarks.map((x: { ID: string; }) => axios.get(`/api/library/song/data/${x.ID}`)))).map(x => x.data);
|
||||
setLibrarySongs(LibSongs);
|
||||
setBookmarkedSongs(BookSongs);
|
||||
setAvailableOverrides(Overrides.data);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
state.UserDetails ?
|
||||
<Box>
|
||||
<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 }); window.location.assign("/") }}>Log out</Button>
|
||||
</PageHeader.Actions>
|
||||
</PageHeader.TitleArea>
|
||||
</PageHeader>
|
||||
<Divider />
|
||||
<Heading sx={{ marginBottom: 2 }}>Active Songs</Heading>
|
||||
<Dialog isOpen={isActivateDialogOpen} onDismiss={() => setIsActivateDialogOpen(false)} aria-labelledby="header">
|
||||
<Dialog.Header id="header">Activate song</Dialog.Header>
|
||||
<Box p={3}>
|
||||
<Text>In order to activate a song for use, you need to sacrifice another song.<br />Please select a song you own to replace:</Text>
|
||||
<ActionMenu>
|
||||
<ActionMenu.Button>Select a song...</ActionMenu.Button>
|
||||
<ActionMenu.Overlay width="medium">
|
||||
<ActionList>
|
||||
{
|
||||
availableOverrides.map(x => {
|
||||
return <ActionList.Item onSelect={async () => {
|
||||
setIsActivateDialogOpen(false);
|
||||
const Res = await axios.post("/api/library/me/activate", { SongID: overriding!.ID!, ToOverride: x.Template });
|
||||
if (Res.status === 200)
|
||||
setLibrarySongs([
|
||||
...librarySongs,
|
||||
{ ...overriding!, Override: x.Template }
|
||||
])
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>
|
||||
{x.Name}
|
||||
</ActionList.Item>;
|
||||
})
|
||||
}
|
||||
</ActionList>
|
||||
</ActionMenu.Overlay>
|
||||
</ActionMenu>
|
||||
</Box>
|
||||
</Dialog>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
librarySongs.length >= 1 ?
|
||||
librarySongs.map(x => {
|
||||
return <Song data={x}>
|
||||
<center>Overriding: <Text sx={{ display: "block", fontWeight: "700", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }}>{availableOverrides.find(y => y.Template.toLowerCase() === x.Override.toLowerCase())?.Name}</Text></center>
|
||||
<Button sx={{ width: "100%", marginTop: 2 }} variant="danger" onClick={async () => {
|
||||
const Res = await axios.post("/api/library/me/deactivate", { SongID: x.ID });
|
||||
if (Res.status === 200) {
|
||||
librarySongs.splice(librarySongs.findIndex(y => y.ID === x.ID), 1);
|
||||
setLibrarySongs([...librarySongs]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Remove from Active</Button>
|
||||
</Song>;
|
||||
})
|
||||
: <Text>You have no activated songs.</Text>
|
||||
}
|
||||
</Box>
|
||||
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>My Subscriptions</Heading>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
bookmarkedSongs.length >= 1 ?
|
||||
bookmarkedSongs.map(x => {
|
||||
return <Song data={x}>
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={() => { setIsActivateDialogOpen(true); setOverriding(x) }} disabled={librarySongs.findIndex(y => y.ID === x.ID) !== -1}>Add to Active</Button>
|
||||
<Button sx={{ width: "100%" }} variant="danger" onClick={async () => {
|
||||
const Res = await axios.post("/api/library/me/unbookmark", { SongID: x.ID });
|
||||
if (Res.status === 200) {
|
||||
bookmarkedSongs.splice(bookmarkedSongs.findIndex(y => y.ID === x.ID), 1);
|
||||
setBookmarkedSongs([...bookmarkedSongs]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Unsubscribe</Button>
|
||||
</Song>;
|
||||
})
|
||||
: <Text>You have no bookmarked songs.</Text>
|
||||
}
|
||||
</Box>
|
||||
</Box> :
|
||||
<>
|
||||
<Text>You are not logged in.<br />Log in using the button in the top right.</Text>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,79 +1,79 @@
|
|||
import { Box, Button, Heading } from "@primer/react";
|
||||
import { Song } from "../components/Song";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import { SiteContext } from "../utils/State";
|
||||
|
||||
export function Tracks() {
|
||||
const [trackData, setTrackData] = useState<{ Header: string, Songs: unknown[] }[]>([]);
|
||||
const [bookmarks, setBookmarks] = useState<unknown[]>([]);
|
||||
const {state} = useContext(SiteContext);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Discovery = await axios.get("/api/discovery");
|
||||
if (Discovery.status !== 200)
|
||||
return toast(Discovery.data.errorMessage, { type: "error" });
|
||||
|
||||
setTrackData(Discovery.data);
|
||||
|
||||
const Bookmarks = await axios.get("/api/library/me");
|
||||
if (Bookmarks.status !== 200)
|
||||
return// toast(Bookmarks.data.errorMessage, { type: "error" });
|
||||
|
||||
setBookmarks(Bookmarks.data.Bookmarks);
|
||||
})();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
trackData.map(x => {
|
||||
return <Box>
|
||||
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>{x.Header}</Heading>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
x.Songs.map(x => {
|
||||
return <Song data={x}>
|
||||
{
|
||||
bookmarks.findIndex(y => y.ID === x.ID) !== -1 ?
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="danger" onClick={async () => {
|
||||
const Res = await axios.post("/api/library/me/unbookmark", { SongID: x.ID });
|
||||
if (Res.status === 200)
|
||||
{
|
||||
bookmarks.splice(bookmarks.findIndex(y => y.ID === x.ID), 1);
|
||||
setBookmarks([...bookmarks]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Unsubscribe</Button> :
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={async () => {
|
||||
if (!state.UserDetails)
|
||||
{
|
||||
if (!state.DiscordOauthURL)
|
||||
return toast("You are not logged in. Please log in first!");
|
||||
|
||||
return window.location.assign(state.DiscordOauthURL);
|
||||
}
|
||||
|
||||
const Res = await axios.post("/api/library/me/bookmark", { SongID: x.ID });
|
||||
if (Res.status === 200)
|
||||
{
|
||||
bookmarks.push(x);
|
||||
setBookmarks([...bookmarks]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Subscribe</Button>
|
||||
}
|
||||
</Song>
|
||||
})
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
})
|
||||
}
|
||||
</>
|
||||
)
|
||||
import { Box, Button, Heading } from "@primer/react";
|
||||
import { Song } from "../components/Song";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import { SiteContext } from "../utils/State";
|
||||
|
||||
export function Tracks() {
|
||||
const [trackData, setTrackData] = useState<{ Header: string, Songs: unknown[] }[]>([]);
|
||||
const [bookmarks, setBookmarks] = useState<unknown[]>([]);
|
||||
const {state} = useContext(SiteContext);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const Discovery = await axios.get("/api/discovery");
|
||||
if (Discovery.status !== 200)
|
||||
return toast(Discovery.data.errorMessage, { type: "error" });
|
||||
|
||||
setTrackData(Discovery.data);
|
||||
|
||||
const Bookmarks = await axios.get("/api/library/me");
|
||||
if (Bookmarks.status !== 200)
|
||||
return// toast(Bookmarks.data.errorMessage, { type: "error" });
|
||||
|
||||
setBookmarks(Bookmarks.data.Bookmarks);
|
||||
})();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
trackData.map(x => {
|
||||
return <Box>
|
||||
<Heading sx={{ marginTop: 2, marginBottom: 2 }}>{x.Header}</Heading>
|
||||
<Box className="songCategory">
|
||||
{
|
||||
x.Songs.map(x => {
|
||||
return <Song data={x}>
|
||||
{
|
||||
bookmarks.findIndex(y => y.ID === x.ID) !== -1 ?
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="danger" onClick={async () => {
|
||||
const Res = await axios.post("/api/library/me/unbookmark", { SongID: x.ID });
|
||||
if (Res.status === 200)
|
||||
{
|
||||
bookmarks.splice(bookmarks.findIndex(y => y.ID === x.ID), 1);
|
||||
setBookmarks([...bookmarks]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Unsubscribe</Button> :
|
||||
<Button sx={{ width: "100%", marginBottom: 1 }} variant="primary" onClick={async () => {
|
||||
if (!state.UserDetails)
|
||||
{
|
||||
if (!state.DiscordOauthURL)
|
||||
return toast("You are not logged in. Please log in first!");
|
||||
|
||||
return window.location.assign(state.DiscordOauthURL);
|
||||
}
|
||||
|
||||
const Res = await axios.post("/api/library/me/bookmark", { SongID: x.ID });
|
||||
if (Res.status === 200)
|
||||
{
|
||||
bookmarks.push(x);
|
||||
setBookmarks([...bookmarks]);
|
||||
}
|
||||
else
|
||||
toast(Res.data.errorMessage, { type: "error" })
|
||||
}}>Subscribe</Button>
|
||||
}
|
||||
</Song>
|
||||
})
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
})
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { AxInstance } from "./Requests";
|
||||
|
||||
export async function VerifyAdminKey(Key: string): Promise<{ Success: boolean; Message: string; }> {
|
||||
const { status, data } = await AxInstance.post("/admin/api/key", { Key });
|
||||
return { Success: status === 200, Message: data };
|
||||
import { AxInstance } from "./Requests";
|
||||
|
||||
export async function VerifyAdminKey(Key: string): Promise<{ Success: boolean; Message: string; }> {
|
||||
const { status, data } = await AxInstance.post("/api/admin/key", { Key });
|
||||
return { Success: status === 200, Message: data };
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
export function moveElement(fromIndex: number, toIndex: number, array: unknown[]) {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||
export function moveElement(fromIndex: number, toIndex: number, array: unknown[]) {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import axios from "axios";
|
||||
// TODO: grab this data from somewhere idk
|
||||
axios.defaults.baseURL = import.meta.env.DEV ? "http://localhost:6677" : "https://partypack.mcthe.dev";
|
||||
axios.defaults.withCredentials = true
|
||||
axios.defaults.validateStatus = () => true;
|
||||
|
||||
import axios from "axios";
|
||||
axios.defaults.baseURL = import.meta.env.VITE_SERVER_ROOT_URL ?? "http://localhost:6677";
|
||||
axios.defaults.withCredentials = true
|
||||
axios.defaults.validateStatus = () => true;
|
||||
|
||||
export const AxInstance = axios.create();
|
8
src/vite-env.d.ts
vendored
8
src/vite-env.d.ts
vendored
|
@ -1 +1,9 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER_ROOT_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
|
@ -15,11 +15,12 @@
|
|||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
/* Fuck you typescript no linting for you */
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
|
|
Loading…
Reference in New Issue
Block a user