diff --git a/Server/.example.env b/Server/.example.env
index c6b325d..0863aef 100644
--- a/Server/.example.env
+++ b/Server/.example.env
@@ -9,6 +9,7 @@ ADMIN_KEY=
BOT_TOKEN=
USE_HTTPS=true # set this to false if you're debugging on your computer
+MAX_AMOUNT_OF_DRAFTS_AT_ONCE=30
DISCORD_SERVER_ID=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
\ No newline at end of file
diff --git a/Server/Source/Handlers/Server.ts b/Server/Source/Handlers/Server.ts
index 2b922df..a29c25d 100644
--- a/Server/Source/Handlers/Server.ts
+++ b/Server/Source/Handlers/Server.ts
@@ -54,12 +54,12 @@ async function Initialize() {
App.use("/assets", e.static("dist/assets"));
}
- App.use("/api/*", (_, res) => res.status(404).json({ errorMessage: "Not Found" }));
+ App.use("/api/*", (_, res) => res.status(404).send("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!" });
+ res.status(500).json(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)") : "")}`));
diff --git a/Server/Source/Modules/Constants.ts b/Server/Source/Modules/Constants.ts
index 4b4a0f5..2453a07 100644
--- a/Server/Source/Modules/Constants.ts
+++ b/Server/Source/Modules/Constants.ts
@@ -17,6 +17,7 @@ export const COOKIE_SIGN_KEY = process.env.COOKIE_SIGN_KEY; // Secret that will
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 MAX_AMOUNT_OF_DRAFTS_AT_ONCE = process.env.MAX_AMOUNT_OF_DRAFTS_AT_ONCE; // A limit for all the drafts you can have.
export const DISCORD_SERVER_ID = process.env.DISCORD_SERVER_ID; // Server ID to check roles for permissions in.
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.
\ No newline at end of file
diff --git a/Server/Source/Modules/Middleware.ts b/Server/Source/Modules/Middleware.ts
index 8936133..0c6431d 100644
--- a/Server/Source/Modules/Middleware.ts
+++ b/Server/Source/Modules/Middleware.ts
@@ -16,7 +16,7 @@ declare global {
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." });
+ return res.status(401).send("This endpoint requires authorization.");
let JWT: JwtPayload;
try {
@@ -28,7 +28,7 @@ export function RequireAuthentication(Relations?: object) {
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." });
+ return res.status(401).send("Invalid Partypack token provided. User does not exist in database. Please contact an instance admin.");
req.user = UserData;
next();
@@ -41,7 +41,7 @@ export function ValidateBody(Schema: j.Schema) {
req.body = await Schema.validateAsync(req.body);
next();
} catch (err) {
- res.status(400).json({ errorMessage: "Body validation failed.", details: err })
+ res.status(400).json(err)
}
}
}
@@ -52,7 +52,7 @@ export function ValidateQuery(Schema: j.Schema) {
req.query = await Schema.validateAsync(req.query);
next();
} catch (err) {
- res.status(400).json({ errorMessage: "Query validation failed.", details: err })
+ res.status(400).json(err)
}
}
}
\ No newline at end of file
diff --git a/Server/Source/Routes/Admin.ts b/Server/Source/Routes/Admin.ts
index 0a06281..5a4dc4e 100644
--- a/Server/Source/Routes/Admin.ts
+++ b/Server/Source/Routes/Admin.ts
@@ -2,7 +2,7 @@
import { FULL_SERVER_ROOT } from "../Modules/Constants";
import { Router } from "express";
import { UserPermissions } from "../Schemas/User";
-import { Song } from "../Schemas/Song";
+import { Song, SongStatus } from "../Schemas/Song";
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
import { writeFileSync } from "fs";
import { ForcedCategory } from "../Schemas/ForcedCategory";
@@ -11,6 +11,7 @@ import { Debug } from "../Modules/Logger";
import { magenta } from "colorette";
import ffmpeg from "fluent-ffmpeg";
import j from "joi";
+import sizeOf from "image-size";
const App = Router();
@@ -21,126 +22,13 @@ const App = Router();
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)
+ if (req.user!.PermissionLevel! < UserPermissions.Administrator)
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, Draft: false, Author: req.user! }).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 ((await fromBuffer(Decoded))?.ext !== "mid")
- 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",
- RequireAuthentication(),
- ValidateBody(j.object({
- Data: j.string().hex().required(),
- TargetSong: j.string().uuid().required()
- })),
- async (req, res) => {
- const Decoded = Buffer.from(req.body.Data, "hex");
- const ext = (await fromBuffer(Decoded))!.ext;
-
- if (!["mp3", "m4a", "ogg", "wav"].includes(ext))
- return res.status(404).send("Invalid audio file. (supported: mp3, m4a, ogg, wav)");
-
- 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.");
-
- await writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
- ffmpeg()
- .input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`)
- .outputOptions([
- "-map 0",
- "-use_timeline 1",
- "-f dash"
- ])
- .output(`./Saved/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`)
- .on("start", cl => Debug(`ffmpeg running with ${magenta(cl)}`))
- .on("end", () => Debug("Ffmpeg finished running"))
- .on("error", (e, stdout, stderr) => { console.error(e); console.log(stdout); console.error(stderr); })
- .run();
-
- res.send("ffmpeg now running on song.");
- });
-
-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");
- const ext = (await fromBuffer(Decoded))!.ext;
-
- if (ext !== "png")
- return res.status(400).send("Invalid image file. (supported: png)");
-
- 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 {
- /*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.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package(true))));
App.post("/update/discovery",
ValidateBody(j.array().items(j.object({
diff --git a/Server/Source/Routes/Authentication.ts b/Server/Source/Routes/Authentication.ts
index 3f7c6e0..0e5ea4c 100644
--- a/Server/Source/Routes/Authentication.ts
+++ b/Server/Source/Routes/Authentication.ts
@@ -62,7 +62,7 @@ async (req, res) => {
}).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")
+ 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, Role: DBUser.PermissionLevel })).toString("hex")
if (req.query.state) {
try {
const Decoded = JSON.parse(Buffer.from(decodeURI(req.query.state as string), "base64").toString("utf-8"));
diff --git a/Server/Source/Routes/Debug.ts b/Server/Source/Routes/Debug.ts
new file mode 100644
index 0000000..824763b
--- /dev/null
+++ b/Server/Source/Routes/Debug.ts
@@ -0,0 +1,44 @@
+import { Router } from "express";
+import { ENVIRONMENT } from "../Modules/Constants";
+import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
+import { UserPermissions } from "../Schemas/User";
+import j from "joi";
+
+const App = Router();
+
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+// ! NEVER EVER ENABLE THESE ON PRODUCTION OR STAGING
+
+App.use((req, res, next) => {
+ if (ENVIRONMENT !== "dev" && ENVIRONMENT !== "debug")
+ return res.status(403).send("The current server environment does not allow for debugging endpoints. Switch to `dev` or `debug` to enable them.");
+
+ next();
+})
+
+App.post("/update/permissions",
+RequireAuthentication(),
+ValidateBody(j.object({
+ Perms: j.number().valid(...(Object.values(UserPermissions).filter(x => !isNaN(Number(x))))).required()
+})),
+async (req, res) => {
+ req.user!.PermissionLevel! = req.body.Perms as UserPermissions;
+ await req.user!.save();
+ res.json(req.user);
+})
+
+export default {
+ App,
+ DefaultAPI: "/api/debug"
+}
\ No newline at end of file
diff --git a/Server/Source/Routes/Downloads.ts b/Server/Source/Routes/Downloads.ts
index c2f3b34..09eee32 100644
--- a/Server/Source/Routes/Downloads.ts
+++ b/Server/Source/Routes/Downloads.ts
@@ -13,12 +13,10 @@ async (req, res) => {
//const Song = AvailableFestivalSongs.find(x => x.UUID === req.params.SongUUID);
const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
if (!SongData)
- return res.status(404).json({ errorMessage: "Song not found." });
-
- console.log(SongData);
+ return res.status(404).send("Song not found.");
if (SongData.IsDraft && SongData.Author.ID !== req.user!.ID)
- return res.status(403).json({ errorMessage: "You cannot use this track, because it's a draft." });
+ return res.status(403).send("You cannot use this track, because it's a draft.");
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`;
switch (req.params.File.toLowerCase()) {
@@ -77,7 +75,7 @@ App.get("/:InternalID", async (req, res, next) => {
return next(); // trust me bro
if (SongData.IsDraft && SongData.Author.ID !== req.user!.ID)
- return res.status(403).json({ errorMessage: "You cannot use this track, because it's a draft." });
+ return res.status(403).send("You cannot use this track, because it's a draft.");
const BaseURL = `${FULL_SERVER_ROOT}/song/download/${SongData.ID}/`;
res.set("content-type", "application/json");
diff --git a/Server/Source/Routes/Drafting.ts b/Server/Source/Routes/Drafting.ts
index d4e24a4..ba45a2b 100644
--- a/Server/Source/Routes/Drafting.ts
+++ b/Server/Source/Routes/Drafting.ts
@@ -1,20 +1,20 @@
import j from "joi";
-import exif from "exif-reader";
import ffmpeg from "fluent-ffmpeg";
+import sizeOf from "image-size";
import { Router } from "express";
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
-import { Song } from "../Schemas/Song";
+import { Song, SongStatus } from "../Schemas/Song";
import { Debug } from "../Modules/Logger";
import { magenta } from "colorette";
import { fromBuffer } from "file-type";
import { rmSync, writeFileSync } from "fs";
-import { FULL_SERVER_ROOT } from "../Modules/Constants";
+import { FULL_SERVER_ROOT, MAX_AMOUNT_OF_DRAFTS_AT_ONCE } from "../Modules/Constants";
import { UserPermissions } from "../Schemas/User";
const App = Router();
App.post("/create",
- RequireAuthentication(),
+ RequireAuthentication({ CreatedTracks: true }),
ValidateBody(j.object({
Name: j.string().required().min(3).max(64),
Year: j.number().required().min(1).max(2999),
@@ -25,15 +25,19 @@ App.post("/create",
Album: j.string().required(),
GuitarStarterType: j.string().valid("Keytar", "Guitar").required(),
Tempo: j.number().min(20).max(1250).required(),
- 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)
+ BassDifficulty: j.number().required().min(0).max(6),
+ GuitarDifficulty: j.number().required().min(0).max(6),
+ DrumsDifficulty: j.number().required().min(0).max(6),
+ VocalsDifficulty: j.number().required().min(0).max(6)
})),
async (req, res) => {
+ if (req.user!.CreatedTracks.length > Number(MAX_AMOUNT_OF_DRAFTS_AT_ONCE))
+ return res.status(400).send("You ran out of free draft spots. Please delete some first.");
+
const SongData = await Song.create({
...req.body,
IsDraft: true,
+ Status: SongStatus.PROCESSING,
Author: req.user!
}).save();
@@ -62,6 +66,10 @@ App.post("/upload/midi",
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Data.mid`, Decoded);
res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/midi.mid`);
+
+ await SongData.reload();
+ SongData.HasMidi = true;
+ await SongData.save();
});
App.post("/upload/cover",
@@ -85,6 +93,15 @@ App.post("/upload/cover",
return res.status(403).send("You don't have permission to upload to this song.");
try {
+ const ImageSize = sizeOf(Decoded);
+ if (!ImageSize.height || !ImageSize.width)
+ throw new Error("Unknown image size error");
+
+ if (ImageSize.height !== ImageSize.width)
+ return res.status(400).send("Image must have a 1:1 ratio.");
+
+ if (ImageSize.width < 512 || ImageSize.width > 2048)
+ return res.status(400).send("Image cannot be smaller than 512x512 pixels and larger than 2048x2048 pixels.");
/*const ImageMetadata = exif(Decoded);
if (!ImageMetadata.Image?.ImageWidth || !ImageMetadata.Image?.ImageLength)
throw new Error("Invalid image file.");
@@ -99,6 +116,10 @@ App.post("/upload/cover",
return res.status(400).send("Invalid image file.");
}
+ await SongData.reload();
+ SongData.HasCover = true;
+ await SongData.save();
+
writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Cover.png`, Decoded);
res.send(`${FULL_SERVER_ROOT}/song/download/${req.body.TargetSong}/cover.png`);
});
@@ -123,6 +144,16 @@ App.post("/upload/audio",
if (req.user!.PermissionLevel! < UserPermissions.Administrator && SongData.Author.ID !== req.user!.ID)
return res.status(403).send("You don't have permission to upload to this song.");
+ if (SongData.HasAudio) {
+ if (SongData.Status !== SongStatus.BROKEN && SongData.Status !== SongStatus.DEFAULT && SongData.Status !== SongStatus.DENIED && SongData.Status !== SongStatus.PUBLIC)
+ return res.status(400).send("You cannot update this song at this moment.");
+
+ rmSync(`./Saved/Songs/${req.body.TargetSong}/Chunks`, { recursive: true });
+ SongData.HasAudio = false;
+ SongData.Status = SongStatus.PROCESSING;
+ await SongData.save();
+ }
+
await writeFileSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`, Decoded);
ffmpeg()
.input(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`)
@@ -133,21 +164,55 @@ App.post("/upload/audio",
])
.output(`./Saved/Songs/${req.body.TargetSong}/Chunks/Manifest.mpd`)
.on("start", cl => Debug(`ffmpeg running with ${magenta(cl)}`))
- .on("end", () => {
+ .on("end", async () => {
Debug("Ffmpeg finished running");
rmSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`);
+
+ await SongData.reload();
+ SongData.HasAudio = true;
+ await SongData.save();
})
- .on("error", (e, stdout, stderr) => {
+ .on("error", async (e, stdout, stderr) => {
console.error(e);
console.log(stdout);
console.error(stderr);
rmSync(`./Saved/Songs/${req.body.TargetSong}/Audio.${ext}`);
+
+ await SongData.reload();
+ SongData.Status = SongStatus.BROKEN;
+ await SongData.save();
})
.run();
res.send("ffmpeg now running on song.");
});
+App.post("/delete",
+ RequireAuthentication(),
+ ValidateBody(j.object({
+ TargetSong: j.string().uuid().required()
+ })),
+ async (req, res) => {
+ const SongData = await Song.findOne({ where: { ID: req.body.TargetSong }, relations: { Author: true } })
+ if (!SongData)
+ return res.status(404).send("The draft you're trying to delete does not exist.");
+
+ const IsAdmin = req.user!.PermissionLevel! >= UserPermissions.Administrator;
+ if (!IsAdmin) {
+ if (SongData.Author.ID !== req.user!.ID)
+ return res.status(403).send("You don't have permission to remove this draft.");
+
+ if (!SongData.IsDraft)
+ return res.status(400).send("This draft has already been published. You need to contact an admin to delete published drafts.");
+
+ if (SongData.Status !== SongStatus.DEFAULT && SongData.Status !== SongStatus.DENIED && SongData.Status !== SongStatus.BROKEN)
+ return res.status(400).send("You cannot delete this draft at this moment.");
+ }
+
+ await SongData.remove();
+ res.send("The draft has been deleted.");
+ });
+
App.post("/submit",
RequireAuthentication(),
ValidateBody(j.object({
@@ -164,11 +229,19 @@ App.post("/submit",
if (!SongData.IsDraft)
return res.status(400).send("This song has already been approved and published.");
- if (SongData.DraftAwaitingReview)
- return res.status(400).send("You already submitted this song for review.");
+ if (SongData.Status === SongStatus.ACCEPTED) {
+ SongData.Status = SongStatus.PUBLIC;
+ SongData.IsDraft = false;
+ await SongData.save();
+ return res.send("Song has been published successfully.");
+ }
+
+ if (SongData.Status !== SongStatus.DEFAULT)
+ return res.status(400).send("You cannot submit this song for review at this time.");
+
+ SongData.Status = req.user!.PermissionLevel! >= UserPermissions.VerifiedUser ? SongStatus.ACCEPTED : SongStatus.AWAITING_REVIEW;
SongData.DraftReviewSubmittedAt = new Date();
- SongData.DraftAwaitingReview = true;
await SongData.save();
return res.send("Song has been submitted for approval by admins.");
diff --git a/Server/Source/Routes/Library.ts b/Server/Source/Routes/Library.ts
index ef5f43a..ff60360 100644
--- a/Server/Source/Routes/Library.ts
+++ b/Server/Source/Routes/Library.ts
@@ -1,17 +1,28 @@
import { Router } from "express";
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
-import { Song } from "../Schemas/Song";
+import { Song, SongStatus } from "../Schemas/Song";
import { OriginalSparks } from "../Modules/FNUtil";
import j from "joi";
import { UserPermissions } from "../Schemas/User";
const App = Router();
-App.get("/me", RequireAuthentication({ BookmarkedSongs: true, CreatedTracks: true }), (req, res) => {
+App.get("/me", RequireAuthentication({ BookmarkedSongs: true, CreatedTracks: true }), async (req, res) => {
+ const ProcessingTracks = req.user!.CreatedTracks.filter(x => x.Status === SongStatus.PROCESSING);
+ if (ProcessingTracks.length > 0)
+ for (const Track of ProcessingTracks) {
+ console.log(Track.HasAudio, Track.HasMidi, Track.HasCover)
+ if (!Track.HasAudio || !Track.HasMidi || !Track.HasCover)
+ continue;
+
+ Track.Status = SongStatus.DEFAULT;
+ await Track.save();
+ }
+
res.json({
- Bookmarks: req.user?.BookmarkedSongs.map(x => x.Package()),
- Created: req.user?.CreatedTracks.map(x => x.Package()),
- Library: req.user?.Library
+ Bookmarks: req.user!.BookmarkedSongs.map(x => x.Package()),
+ Created: req.user!.CreatedTracks.map(x => x.Package(true)),
+ Library: req.user!.Library
})
})
@@ -23,17 +34,17 @@ ValidateBody(j.object({
})),
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." });
+ return res.status(400).send("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." });
+ return res.status(400).send("This song is already activated.");
const SongData = await Song.findOne({ where: { ID: req.body.SongID }, relations: { Author: true } });
if (!SongData)
- return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
+ return res.status(404).send("Provided song doesn't exist.");
- if (SongData.IsDraft && (req.user!.PermissionLevel < UserPermissions.Administrator && SongData.Author.ID !== req.user!.ID))
- return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." });
+ if (SongData.IsDraft && (req.user!.PermissionLevel < UserPermissions.TrackVerifier && SongData.Author.ID !== req.user!.ID))
+ return res.status(403).send("You cannot activate this track, because it's a draft.");
req.user!.Library.push({ SongID: req.body.SongID.toLowerCase(), Overriding: req.body.ToOverride.toLowerCase() });
req.user!.save();
@@ -49,7 +60,7 @@ ValidateBody(j.object({
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." });
+ return res.status(400).send("This song is not activated.");
req.user?.Library.splice(idx, 1);
req.user?.save();
@@ -64,14 +75,14 @@ ValidateBody(j.object({
})),
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." });
+ return res.status(400).send("You're already subscribed to this song.");
const SongData = await Song.findOne({ where: { ID: req.body.SongID }, relations: { Author: true } });
if (!SongData)
- return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
+ return res.status(404).send("Provided song doesn't exist.");
- if (SongData.IsDraft && (req.user.PermissionLevel < UserPermissions.Administrator && SongData.Author.ID !== req.user.ID))
- return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." });
+ if (SongData.IsDraft && (req.user.PermissionLevel < UserPermissions.TrackVerifier && SongData.Author.ID !== req.user.ID))
+ return res.status(403).send("You cannot subscribe to this track, because it's a draft.");
req.user?.BookmarkedSongs.push(SongData);
req.user?.save();
@@ -87,7 +98,7 @@ ValidateBody(j.object({
async (req, res) => {
const idx = req.user!.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase());
if (idx === -1)
- return res.status(400).json({ errorMessage: "You aren't subscribed to this song." });
+ return res.status(400).send("You aren't subscribed to this song.");
req.user?.BookmarkedSongs.splice(idx, 1);
req.user?.save();
@@ -98,12 +109,12 @@ async (req, res) => {
App.get("/song/data/:InternalID",
RequireAuthentication(),
async (req, res) => {
- const SongData = await Song.findOne({ where: { ID: req.body.SongID }, relations: { Author: true } });
+ const SongData = await Song.findOne({ where: { ID: req.params.InternalID }, relations: { Author: true } });
if (!SongData)
- return res.status(404).json({ errorMessage: "Provided song doesn't exist." });
+ return res.status(404).send("Provided song doesn't exist.");
- if (SongData.IsDraft && (req.user!.PermissionLevel < UserPermissions.Administrator && SongData.Author.ID !== req.user!.ID))
- return res.status(403).json({ errorMessage: "You cannot subscribe to this track, because it's a draft." });
+ if (SongData.IsDraft && (req.user!.PermissionLevel < UserPermissions.TrackVerifier && SongData.Author.ID !== req.user!.ID))
+ return res.status(403).send("You cannot use this track, because it's a draft.");
res.json(SongData.Package());
})
diff --git a/Server/Source/Routes/Moderation.ts b/Server/Source/Routes/Moderation.ts
new file mode 100644
index 0000000..207d05e
--- /dev/null
+++ b/Server/Source/Routes/Moderation.ts
@@ -0,0 +1,60 @@
+import j from "joi";
+import { NextFunction, Request, Response, Router } from "express";
+import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
+import { UserPermissions } from "../Schemas/User";
+import { Song, SongStatus } from "../Schemas/Song";
+
+const App = Router();
+
+App.use(RequireAuthentication());
+
+function PermsLevel(Perms: UserPermissions = UserPermissions.Moderator) {
+ return (req: Request, res: Response, next: NextFunction) => {
+ if (!req.user)
+ return res.status(403).send();
+
+ if (req.user.PermissionLevel < Perms)
+ return res.status(403).send("You don't have permission to access this endpoint.");
+
+ next();
+ };
+}
+
+App.get("/submissions",
+PermsLevel(UserPermissions.TrackVerifier),
+async (_, res) => res.json((await Song.find({ where: { IsDraft: true, Status: SongStatus.AWAITING_REVIEW }, order: { DraftReviewSubmittedAt: "ASC" } })).map(x => x.Package(true))));
+
+App.post("/submissions/:Action",
+PermsLevel(UserPermissions.TrackVerifier),
+ValidateBody(j.object({
+ SongID: j.string().uuid().required()
+})),
+async (req, res) => {
+ const SongData = await Song.findOne({ where: { ID: req.body.SongID } });
+ if (!SongData)
+ return res.status(404).send("This song does not exist anymore.");
+
+ if (req.params.Action !== "deny" && req.params.Action !== "accept")
+ return res.status(400).send("Invalid action requested.");
+
+ if (SongData.Status !== SongStatus.AWAITING_REVIEW)
+ return res.status(400).send("This song is no longer awaiting a review.");
+
+ switch (req.params.Action) {
+ case "accept":
+ SongData.Status = SongStatus.ACCEPTED;
+ break;
+
+ case "deny":
+ SongData.Status = SongStatus.DENIED;
+ break;
+ }
+
+ await SongData.save();
+ res.send("Successfully changed song status.");
+});
+
+export default {
+ App,
+ DefaultAPI: "/api/moderation"
+}
\ No newline at end of file
diff --git a/Server/Source/Routes/Pages.ts b/Server/Source/Routes/Pages.ts
index 607a5e3..140339c 100644
--- a/Server/Source/Routes/Pages.ts
+++ b/Server/Source/Routes/Pages.ts
@@ -40,7 +40,7 @@ App.get("/content/api/pages/fortnite-game/:Section", RequireAuthentication(), as
const CachedSection = Object.values(FullFortnitePages!).find(x => x._title === req.params.Section);
if (!CachedSection)
- return res.status(404).json({ errorMessage: "funny section not found haha kill me" });
+ return res.status(404).send("funny section not found haha kill me");
const ContentFromServer = await axios.get(`https://fortnitecontent-website-prod07.ol.epicgames.com/content/api/pages/fortnite-game/${CachedSection._title}`);
if (ContentFromServer.status !== 200)
diff --git a/Server/Source/Routes/Ratings.ts b/Server/Source/Routes/Ratings.ts
index 1cfbc9e..8fe47ba 100644
--- a/Server/Source/Routes/Ratings.ts
+++ b/Server/Source/Routes/Ratings.ts
@@ -9,7 +9,7 @@ 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." });
+ return res.status(404).send("The song you're trying to get the rating for has not been found.");
let Average = 0;
if (SongData.Ratings.length > 0) {
@@ -31,7 +31,7 @@ ValidateBody(j.object({
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." });
+ return res.status(404).send("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)
diff --git a/Server/Source/Schemas/Song.ts b/Server/Source/Schemas/Song.ts
index 1ac38e6..2c68b83 100644
--- a/Server/Source/Schemas/Song.ts
+++ b/Server/Source/Schemas/Song.ts
@@ -6,6 +6,16 @@ import { v4 } from "uuid";
import { User } from "./User";
import { join } from "path";
+export enum SongStatus {
+ BROKEN = -100,
+ DEFAULT = 0,
+ PROCESSING = 100,
+ PUBLIC = 200,
+ AWAITING_REVIEW = 300,
+ ACCEPTED = 400,
+ DENIED = 500,
+}
+
@Entity()
export class Song extends BaseEntity {
@PrimaryGeneratedColumn("uuid")
@@ -66,11 +76,20 @@ export class Song extends BaseEntity {
IsDraft: boolean;
@Column({ default: false })
- DraftAwaitingReview: boolean;
+ HasMidi: boolean;
+
+ @Column({ default: false })
+ HasCover: boolean;
+
+ @Column({ default: false })
+ HasAudio: boolean;
@Column({ nullable: true })
DraftReviewSubmittedAt?: Date;
+ @Column({ default: SongStatus.DEFAULT })
+ Status: SongStatus;
+
@Column()
CreationDate: Date;
@@ -96,9 +115,10 @@ export class Song extends BaseEntity {
rmSync(this.Directory, { recursive: true, force: true }); // lets hope this does not cause steam launcher for linux 2.0
}
- public Package() {
+ public Package(IncludeStatus: boolean = false) {
return {
...this,
+ Status: IncludeStatus ? this.Status : SongStatus.DEFAULT,
Directory: undefined, // we should NOT reveal that
Midi: this.Midi ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/midi.mid`,
Cover: this.Cover ?? `${FULL_SERVER_ROOT}/song/download/${this.ID}/cover.png`
diff --git a/Server/Source/Schemas/User.ts b/Server/Source/Schemas/User.ts
index a581118..88a5389 100644
--- a/Server/Source/Schemas/User.ts
+++ b/Server/Source/Schemas/User.ts
@@ -5,6 +5,7 @@ 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,
+ TrackVerifier = 250,
Moderator = 300,
Administrator = 400
}
diff --git a/Server/package-lock.json b/Server/package-lock.json
index f07b39c..02e7d6f 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -15,10 +15,10 @@
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
- "exif-reader": "^2.0.0",
"express": "^4.18.2",
"file-type": "^16.5.3",
"fluent-ffmpeg": "^2.1.2",
+ "image-size": "^1.1.1",
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
"typeorm": "^0.3.19",
@@ -917,11 +917,6 @@
"node": ">= 0.6"
}
},
- "node_modules/exif-reader": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-2.0.0.tgz",
- "integrity": "sha512-/c4JW2Kb4/MdmUJyMKTXhNG524q0pCA1DDCEb2aawd9j2i2+bc+mKGKKyO4z2glRTKW93Ks4BWCgmKeEyKiUeA=="
- },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -1244,6 +1239,20 @@
}
]
},
+ "node_modules/image-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz",
+ "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==",
+ "dependencies": {
+ "queue": "6.0.2"
+ },
+ "bin": {
+ "image-size": "bin/image-size.js"
+ },
+ "engines": {
+ "node": ">=16.x"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -1719,6 +1728,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/queue": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
+ "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
+ "dependencies": {
+ "inherits": "~2.0.3"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
diff --git a/Server/package.json b/Server/package.json
index a0847b4..737dac9 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -47,10 +47,10 @@
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
- "exif-reader": "^2.0.0",
"express": "^4.18.2",
"file-type": "^16.5.3",
"fluent-ffmpeg": "^2.1.2",
+ "image-size": "^1.1.1",
"joi": "^17.12.0",
"jsonwebtoken": "^9.0.2",
"typeorm": "^0.3.19",
diff --git a/src/App.tsx b/src/App.tsx
index 452ce3c..aab3721 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,6 +4,8 @@ import { ToastContainer } from "react-toastify";
import { BaseStyles, ThemeProvider, theme } from "@primer/react";
import { SiteHeader } from "./components/SiteHeader";
import { VerifyAdmin } from "./components/VerifyAdmin";
+import { VerifyRole } from "./components/VerifyRole";
+import { UserPermissions } from "./utils/Extensions";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { CookiesProvider } from "react-cookie";
import { Home } from "./routes/Home";
@@ -14,7 +16,7 @@ import { Profile } from "./routes/Profile";
import { NotFound } from "./routes/404";
import { AdminHome } from "./routes/AdminHome";
import { AdminTrackList } from "./routes/AdminTrackList";
-import { AdminCreateTrack } from "./routes/AdminCreateTrack";
+import { AdminSubmissions } from "./routes/AdminSubmissions";
import { AdminFeaturedTab } from "./routes/AdminFeaturedTab";
import { SiteContext, SiteState } from "./utils/State";
import merge from "deepmerge";
@@ -46,10 +48,12 @@ function App() {
} />
} />
+ {/* Staff routes */}
+ } />
+
{/* Admin routes */}
} />
} />
- } />
} />
diff --git a/src/components/Song.tsx b/src/components/Song.tsx
index eb9a90e..bcbc169 100644
--- a/src/components/Song.tsx
+++ b/src/components/Song.tsx
@@ -1,7 +1,53 @@
import { Box, Label, Text } from "@primer/react";
import { Divider } from "@primer/react/lib-esm/ActionList/Divider";
+import { SongStatus } from "../utils/Extensions";
+import { LabelColorOptions } from "@primer/react/lib-esm/Label/Label";
export function Song({ data, children }: { data: any, children?: JSX.Element[] | JSX.Element | string }) {
+ function GetStatusLabel() {
+ let Variant: LabelColorOptions = "default";
+ let LabelStr = "";
+ switch (data.Status) {
+ case SongStatus.AWAITING_REVIEW:
+ Variant = "accent";
+ LabelStr = "Awaiting review";
+ break;
+ case SongStatus.BROKEN:
+ Variant = "severe";
+ LabelStr = "Missing assets";
+ break;
+ case SongStatus.DEFAULT:
+ // no label unless draft
+ Variant = "danger";
+ if (data.IsDraft)
+ LabelStr = "Draft - not published"
+ break;
+ case SongStatus.PUBLIC:
+ Variant = "success";
+ LabelStr = "Published";
+ break;
+ case SongStatus.PROCESSING:
+ Variant = "done";
+ LabelStr = "Assets processing";
+ break;
+ case SongStatus.ACCEPTED:
+ Variant = "sponsors";
+ LabelStr = "Ready for publishing";
+ break;
+ case SongStatus.DENIED:
+ Variant = "danger";
+ LabelStr = "ACTION NEEDED - Denied";
+ break;
+ //default:
+ //LabelStr = `Unimplemented: ${data.Status}`;
+ //break;
+ }
+
+ return (
+ LabelStr !== "" ? : <>>
+ )
+ }
+
return (
@@ -9,11 +55,7 @@ export function Song({ data, children }: { data: any, children?: JSX.Element[] |
{data.ArtistName}
{data.Name}
{
- data.IsDraft ?
- data.DraftAwaitingReview ?
- :
- :
- <>>
+ GetStatusLabel()
}
{
children ? : <>>
diff --git a/src/components/VerifyRole.tsx b/src/components/VerifyRole.tsx
new file mode 100644
index 0000000..fb86dc8
--- /dev/null
+++ b/src/components/VerifyRole.tsx
@@ -0,0 +1,17 @@
+import { useContext } from "react";
+import { Navigate } from "react-router-dom";
+import { SiteContext } from "../utils/State";
+import { toast } from "react-toastify";
+import { UserPermissions } from "../utils/Extensions";
+
+export function VerifyRole({ children, role }: { children: JSX.Element, role: UserPermissions }) {
+ const {state} = useContext(SiteContext);
+
+ if (state.UserDetails?.Role < role)
+ {
+ toast("Your account does not have the Partypack role required to access this page. Try again later!", { type: "error" });
+ return ;
+ }
+
+ return children;
+}
\ No newline at end of file
diff --git a/src/routes/AdminCreateTrack.tsx b/src/routes/AdminCreateTrack.tsx
deleted file mode 100644
index 89d0ba5..0000000
--- a/src/routes/AdminCreateTrack.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-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(null);
- const [Key, setKey] = useState("Select a key...");
- const [Scale, setScale] = useState("Select a scale...");
- const [GuitarStarterType, setGuitarStarterType] = useState("Select the starter type...");
-
- return (
- <>
- [ADMIN] Create a New Track
-
- >
- )
-}
\ No newline at end of file
diff --git a/src/routes/AdminSubmissions.tsx b/src/routes/AdminSubmissions.tsx
index 303f4e4..352707b 100644
--- a/src/routes/AdminSubmissions.tsx
+++ b/src/routes/AdminSubmissions.tsx
@@ -1,22 +1,81 @@
-import { useEffect } from "react";
-import { toast } from "react-toastify";
-import axios from "axios";
-
-export function AdminSubmissions() {
- useEffect(() => {
- (async () => {
- const Data = await axios.get("/api/admin/");
- const Overrides = await axios.get("/api/library/available");
-
- if (Data.status !== 200 || Overrides.status !== 200)
- return toast("An error has occured while getting the submitted songs!", { type: "error" });
-
-
- })();
- }, []);
-
- return (
- <>
- >
- )
+import { useEffect, useState } from "react";
+import { toast } from "react-toastify";
+import { Heading, Box, Button } from "@primer/react";
+import axios from "axios";
+import { Song } from "../components/Song";
+
+export function AdminSubmissions() {
+ const [submissions, setSubmissions] = useState([]);
+ const [bookmarks, setBookmarks] = useState([]);
+
+ useEffect(() => {
+ (async () => {
+ const Data = await axios.get("/api/moderation/submissions");
+ const Overrides = await axios.get("/api/library/available");
+
+ if (Data.status !== 200 || Overrides.status !== 200)
+ return toast("An error has occured while getting the submitted songs!", { type: "error" });
+
+ setSubmissions(Data.data);
+ })();
+ }, []);
+
+ return (
+ <>
+ Submissions waiting for review
+
+ {
+ submissions.map(x => {
+ return (
+
+ {
+ bookmarks.findIndex(y => y.ID === x.ID) !== -1 ?
+ :
+
+ }
+
+
+
+ )
+ })
+ }
+
+ >
+ )
}
\ No newline at end of file
diff --git a/src/routes/AdminTrackList.tsx b/src/routes/AdminTrackList.tsx
index 3f053d8..0778d9d 100644
--- a/src/routes/AdminTrackList.tsx
+++ b/src/routes/AdminTrackList.tsx
@@ -21,7 +21,7 @@ export function AdminTrackList() {
return (
<>
- [ADMIN] All tracks
+ [ADMIN] All tracks
{
tracks.map(x => {
diff --git a/src/routes/Profile.tsx b/src/routes/Profile.tsx
index 57af646..e4c9814 100644
--- a/src/routes/Profile.tsx
+++ b/src/routes/Profile.tsx
@@ -1,14 +1,18 @@
-import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, Heading, Text } from "@primer/react"
+import { ActionList, ActionMenu, Avatar, Box, Button, Dialog, FormControl, Heading, Text, TextInput } 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 { useContext, useEffect, useRef, useState } from "react";
import { SiteContext } from "../utils/State";
import { useCookies } from "react-cookie";
import { Song } from "../components/Song";
import axios from "axios";
import { toast } from "react-toastify";
+import { SongStatus } from "../utils/Extensions";
+
+const formControlStyle = { paddingTop: 3 };
export function Profile() {
+ const formRef = useRef(null);
const { state, setState } = useContext(SiteContext);
const [, , removeCookie] = useCookies();
const [isActivateDialogOpen, setIsActivateDialogOpen] = useState(false);
@@ -17,6 +21,8 @@ export function Profile() {
const [draftsSongs, setDraftsSongs] = useState([]);
const [availableOverrides, setAvailableOverrides] = useState<{ Name: string, Template: string }[]>([]);
const [overriding, setOverriding] = useState({});
+ const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);
+ const [updating, setUpdating] = useState({});
useEffect(() => {
(async () => {
@@ -26,7 +32,17 @@ export function Profile() {
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 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 }
+ });
+
setLibrarySongs(LibSongs);
setBookmarkedSongs(Data.data.Bookmarks);
setDraftsSongs(Data.data.Created);
@@ -52,8 +68,46 @@ export function Profile() {
+
- Active Songs
+
+
+
+
+ Active Songs
{
librarySongs.length >= 1 ?
@@ -104,6 +160,7 @@ export function Profile() {
: You have no activated songs.
}
+
My Subscriptions
{
@@ -125,32 +182,34 @@ export function Profile() {
: You have no bookmarked songs.
}
+
My Drafts & Published Songs
{
draftsSongs.length >= 1 ?
draftsSongs.map((x, i) => {
return
-
-
;
})
: You have no bookmarked songs.
diff --git a/src/routes/TrackSubmission.tsx b/src/routes/TrackSubmission.tsx
index 876b1e7..8d7f361 100644
--- a/src/routes/TrackSubmission.tsx
+++ b/src/routes/TrackSubmission.tsx
@@ -143,7 +143,7 @@ export function TrackSubmission() {
const VocalsDifficulty = (formRef.current[14] as HTMLInputElement).valueAsNumber;
const BassDifficulty = (formRef.current[15] as HTMLInputElement).valueAsNumber;
- const SongData = await axios.post("/api/drafts/create", {
+ const B = {
Name,
ArtistName,
Album,
@@ -157,7 +157,12 @@ export function TrackSubmission() {
DrumsDifficulty,
VocalsDifficulty,
BassDifficulty
- });
+ };
+
+ if (Object.values(B).includes(NaN) || Object.values(B).includes(null) || Object.values(B).includes(undefined))
+ return toast("One or more required fields missing.", { type: "error" });
+
+ const SongData = await axios.post("/api/drafts/create", B);
toast(SongData.data, { type: SongData.status === 200 ? "success" : "error" });
diff --git a/src/utils/Extensions.ts b/src/utils/Extensions.ts
index d51b296..bcc2db3 100644
--- a/src/utils/Extensions.ts
+++ b/src/utils/Extensions.ts
@@ -1,3 +1,21 @@
export function moveElement(fromIndex: number, toIndex: number, array: unknown[]) {
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0]);
+}
+
+export enum SongStatus {
+ BROKEN = -100,
+ DEFAULT = 0,
+ PROCESSING = 100,
+ PUBLIC = 200,
+ AWAITING_REVIEW = 300,
+ ACCEPTED = 400,
+ DENIED = 500
+}
+
+export enum UserPermissions {
+ User = 100,
+ VerifiedUser = 200,
+ TrackVerifier = 250,
+ Moderator = 300,
+ Administrator = 400
}
\ No newline at end of file
diff --git a/src/utils/State.ts b/src/utils/State.ts
index 171ef30..1f5b080 100644
--- a/src/utils/State.ts
+++ b/src/utils/State.ts
@@ -1,9 +1,10 @@
// massive shoutout to not-nullptr for the site state code from our old project
import { createContext } from "react";
+import { UserPermissions } from "./Extensions";
export const SiteContext = createContext({} as IContext);
-export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string, IsAdmin: boolean }
+export interface UserDetailInterface { ID: string, Username: string, GlobalName: string, Avatar: string, IsAdmin: boolean, Role: UserPermissions }
export interface SiteState {
UserDetails: UserDetailInterface | null;