shtuff
This commit is contained in:
parent
90b2f05883
commit
62407f3d3d
|
@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from "express";
|
||||||
import { User } from "../Schemas/User";
|
import { User } from "../Schemas/User";
|
||||||
import { JwtPayload, verify } from "jsonwebtoken";
|
import { JwtPayload, verify } from "jsonwebtoken";
|
||||||
import { IS_DEBUG, JWT_KEY } from "./Constants";
|
import { IS_DEBUG, JWT_KEY } from "./Constants";
|
||||||
|
import j from "joi";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
@ -13,7 +14,7 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RequireAuthentication(Relations?: object) {
|
export function RequireAuthentication(Relations?: object) {
|
||||||
return async (req: Request, res: Response, next: NextFunction) =>{
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (!req.header("X-Partypack-Token") && !req.cookies["Token"] && !req.header("Authorization"))
|
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).json({ errorMessage: "This endpoint requires authorization." });
|
||||||
|
|
||||||
|
@ -32,4 +33,15 @@ export function RequireAuthentication(Relations?: object) {
|
||||||
req.user = UserData;
|
req.user = UserData;
|
||||||
next();
|
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,6 +1,10 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { ADMIN_KEY } from "../Modules/Constants";
|
import { ADMIN_KEY, FULL_SERVER_ROOT } from "../Modules/Constants";
|
||||||
import { Song } from "../Schemas/Song";
|
import { Song } from "../Schemas/Song";
|
||||||
|
import { ValidateBody } from "../Modules/Middleware";
|
||||||
|
import { writeFileSync } from "fs";
|
||||||
|
import exif from "exif-reader";
|
||||||
|
import j from "joi";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
|
@ -22,7 +26,77 @@ App.get("/test", (_, res) => res.send("Permission check OK"));
|
||||||
|
|
||||||
App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package())));
|
App.get("/tracks", async (_, res) => res.json((await Song.find()).map(x => x.Package())));
|
||||||
|
|
||||||
App.post("/create/song", async (req, res) => res.json(await Song.create(req.body).save()));
|
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/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`);
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
App,
|
App,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { RequireAuthentication } from "../Modules/Middleware";
|
import { RequireAuthentication, ValidateBody } from "../Modules/Middleware";
|
||||||
import { Song } from "../Schemas/Song";
|
import { Song } from "../Schemas/Song";
|
||||||
import { OriginalSparks } from "../Modules/FNUtil";
|
import { OriginalSparks } from "../Modules/FNUtil";
|
||||||
|
import j from "joi";
|
||||||
|
|
||||||
const App = Router();
|
const App = Router();
|
||||||
|
|
||||||
|
@ -12,13 +13,13 @@ App.get("/me", RequireAuthentication({ BookmarkedSongs: true }), (req, res) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
App.post("/me/activate", RequireAuthentication(), async (req, res) => {
|
App.post("/me/activate",
|
||||||
if (!req.body.SongID || !req.body.ToOverride)
|
RequireAuthentication(),
|
||||||
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
|
ValidateBody(j.object({
|
||||||
|
SongID: j.string().uuid().required(),
|
||||||
if (!/^sid_placeholder_(\d){1,3}$/gi.test(req.body.ToOverride))
|
ToOverride: j.string().pattern(/^sid_placeholder_(\d){1,3}$/i).required()
|
||||||
return res.status(400).json({ errorMessage: "Field \"ToOverride\" must match \"sid_placeholder_<number>\"" });
|
})),
|
||||||
|
async (req, res) => {
|
||||||
if (req.user?.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase() || x.Overriding.toLowerCase() === req.body.ToOverride.toLowerCase()) !== -1)
|
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).json({ errorMessage: "This song is already activated." });
|
||||||
|
|
||||||
|
@ -31,10 +32,12 @@ App.post("/me/activate", RequireAuthentication(), async (req, res) => {
|
||||||
res.json(req.user?.Library);
|
res.json(req.user?.Library);
|
||||||
})
|
})
|
||||||
|
|
||||||
App.post("/me/deactivate", RequireAuthentication(), async (req, res) => {
|
App.post("/me/deactivate",
|
||||||
if (!req.body.SongID)
|
RequireAuthentication(),
|
||||||
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
|
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());
|
const idx = req.user!.Library.findIndex(x => x.SongID.toLowerCase() === req.body.SongID.toLowerCase());
|
||||||
if (idx === -1)
|
if (idx === -1)
|
||||||
return res.status(400).json({ errorMessage: "This song is not activated." });
|
return res.status(400).json({ errorMessage: "This song is not activated." });
|
||||||
|
@ -45,10 +48,12 @@ App.post("/me/deactivate", RequireAuthentication(), async (req, res) => {
|
||||||
res.json(req.user?.Library);
|
res.json(req.user?.Library);
|
||||||
})
|
})
|
||||||
|
|
||||||
App.post("/me/bookmark", RequireAuthentication({ BookmarkedSongs: true }), async (req, res) => {
|
App.post("/me/bookmark",
|
||||||
if (!req.body.SongID)
|
RequireAuthentication({ BookmarkedSongs: true }),
|
||||||
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
|
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)
|
if (req.user?.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase()) !== -1)
|
||||||
return res.status(400).json({ errorMessage: "This song is already bookmarked." });
|
return res.status(400).json({ errorMessage: "This song is already bookmarked." });
|
||||||
|
|
||||||
|
@ -62,10 +67,12 @@ App.post("/me/bookmark", RequireAuthentication({ BookmarkedSongs: true }), async
|
||||||
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
|
res.json(req.user?.BookmarkedSongs.map(x => x.Package()));
|
||||||
})
|
})
|
||||||
|
|
||||||
App.post("/me/unbookmark", RequireAuthentication(), async (req, res) => {
|
App.post("/me/unbookmark",
|
||||||
if (!req.body.SongID)
|
RequireAuthentication(),
|
||||||
return res.status(400).json({ errorMessage: "You didn't provide a Song ID." });
|
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());
|
const idx = req.user!.BookmarkedSongs.findIndex(x => x.ID.toLowerCase() === req.body.SongID.toLowerCase());
|
||||||
if (idx === -1)
|
if (idx === -1)
|
||||||
return res.status(400).json({ errorMessage: "This song is not bookmarked." });
|
return res.status(400).json({ errorMessage: "This song is not bookmarked." });
|
||||||
|
|
18
Server/Source/Schemas/Rating.ts
Normal file
18
Server/Source/Schemas/Rating.ts
Normal file
|
@ -0,0 +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;
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
import { BaseEntity, BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { BaseEntity, BeforeInsert, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
import { FULL_SERVER_ROOT } from "../Modules/Constants";
|
||||||
|
import { Rating } from "./Rating";
|
||||||
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Song extends BaseEntity {
|
export class Song extends BaseEntity {
|
||||||
|
@ -60,8 +63,16 @@ export class Song extends BaseEntity {
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
Lipsync?: string;
|
Lipsync?: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Rating, R => R.Rated)
|
||||||
|
Ratings: Rating[];
|
||||||
|
|
||||||
@BeforeInsert()
|
@BeforeInsert()
|
||||||
Setup() {
|
Setup() {
|
||||||
|
this.ID = v4();
|
||||||
|
this.Directory = `./Saved/Songs/${this.ID}`;
|
||||||
|
if (!existsSync(this.Directory))
|
||||||
|
mkdirSync(this.Directory);
|
||||||
|
|
||||||
this.CreationDate = new Date();
|
this.CreationDate = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryColumn } from "typeorm";
|
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
|
||||||
import { Song } from "./Song";
|
import { Song } from "./Song";
|
||||||
|
import { Rating } from "./Rating";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User extends BaseEntity {
|
export class User extends BaseEntity {
|
||||||
|
@ -9,6 +10,9 @@ export class User extends BaseEntity {
|
||||||
@Column({ type: "simple-json" })
|
@Column({ type: "simple-json" })
|
||||||
Library: { SongID: string, Overriding: string }[];
|
Library: { SongID: string, Overriding: string }[];
|
||||||
|
|
||||||
|
@OneToMany(() => Rating, R => R.Author)
|
||||||
|
Ratings: Rating[];
|
||||||
|
|
||||||
@ManyToMany(() => Song, { eager: true })
|
@ManyToMany(() => Song, { eager: true })
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
BookmarkedSongs: Song[];
|
BookmarkedSongs: Song[];
|
||||||
|
|
59
Server/package-lock.json
generated
59
Server/package-lock.json
generated
|
@ -10,16 +10,20 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
"better-sqlite3": "^9.3.0",
|
"better-sqlite3": "^9.3.0",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"exif-reader": "^2.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"joi": "^17.12.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"typeorm": "^0.3.19",
|
"typeorm": "^0.3.19",
|
||||||
"underscore": "^1.13.6"
|
"underscore": "^1.13.6",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.6",
|
"@types/cookie-parser": "^1.4.6",
|
||||||
|
@ -31,6 +35,19 @@
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hapi/hoek": {
|
||||||
|
"version": "9.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||||
|
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@hapi/topo": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@hapi/hoek": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
|
@ -56,6 +73,24 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sideway/address": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@hapi/hoek": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sideway/formula": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
|
||||||
|
},
|
||||||
|
"node_modules/@sideway/pinpoint": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||||
|
},
|
||||||
"node_modules/@sqltools/formatter": {
|
"node_modules/@sqltools/formatter": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
|
||||||
|
@ -186,6 +221,11 @@
|
||||||
"integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==",
|
"integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "9.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz",
|
||||||
|
"integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g=="
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
@ -852,6 +892,11 @@
|
||||||
"node": ">= 0.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": {
|
"node_modules/expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
@ -1183,6 +1228,18 @@
|
||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/joi": {
|
||||||
|
"version": "17.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.0.tgz",
|
||||||
|
"integrity": "sha512-HSLsmSmXz+PV9PYoi3p7cgIbj06WnEBNT28n+bbBNcPZXZFqCzzvGqpTBPujx/Z0nh1+KNQPDrNgdmQ8dq0qYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@hapi/hoek": "^9.3.0",
|
||||||
|
"@hapi/topo": "^5.1.0",
|
||||||
|
"@sideway/address": "^4.1.4",
|
||||||
|
"@sideway/formula": "^3.0.1",
|
||||||
|
"@sideway/pinpoint": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsonwebtoken": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
|
|
|
@ -39,16 +39,20 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/uuid": "^9.0.7",
|
||||||
"axios": "^1.6.5",
|
"axios": "^1.6.5",
|
||||||
"better-sqlite3": "^9.3.0",
|
"better-sqlite3": "^9.3.0",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"exif-reader": "^2.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"joi": "^17.12.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"typeorm": "^0.3.19",
|
"typeorm": "^0.3.19",
|
||||||
"underscore": "^1.13.6"
|
"underscore": "^1.13.6",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/McMistrzYT/BasedServer#readme"
|
"homepage": "https://github.com/McMistrzYT/BasedServer#readme"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useState } from "react";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { BaseStyles, ThemeProvider, theme } from "@primer/react";
|
import { BaseStyles, ThemeProvider, theme } from "@primer/react";
|
||||||
import { SiteHeader } from "./components/SiteHeader";
|
import { SiteHeader } from "./components/SiteHeader";
|
||||||
|
@ -11,12 +12,12 @@ import { AdminLogin } from "./routes/AdminLogin";
|
||||||
import { Download } from "./routes/Download";
|
import { Download } from "./routes/Download";
|
||||||
import { Tracks } from "./routes/Tracks";
|
import { Tracks } from "./routes/Tracks";
|
||||||
import { Profile } from "./routes/Profile";
|
import { Profile } from "./routes/Profile";
|
||||||
|
import { AdminCreateTrack } from "./routes/AdminCreateTrack";
|
||||||
import { SiteContext, SiteState } from "./utils/State";
|
import { SiteContext, SiteState } from "./utils/State";
|
||||||
import merge from "deepmerge";
|
import merge from "deepmerge";
|
||||||
|
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "./css/index.css";
|
import "./css/index.css";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
const DefaultTheme = merge(theme, {}); // we'll use this!! eventually!!!
|
const DefaultTheme = merge(theme, {}); // we'll use this!! eventually!!!
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ function App() {
|
||||||
<Route path="/admin" element={<VerifyAdmin><AdminHome /></VerifyAdmin>} />
|
<Route path="/admin" element={<VerifyAdmin><AdminHome /></VerifyAdmin>} />
|
||||||
<Route path="/admin/login" element={<AdminLogin />} /> {/* this is the only publically available admin endpoint */}
|
<Route path="/admin/login" element={<AdminLogin />} /> {/* this is the only publically available admin endpoint */}
|
||||||
<Route path="/admin/tracks" element={<VerifyAdmin><AdminTrackList /></VerifyAdmin>} />
|
<Route path="/admin/tracks" element={<VerifyAdmin><AdminTrackList /></VerifyAdmin>} />
|
||||||
|
<Route path="/admin/tracks/create" element={<VerifyAdmin><AdminCreateTrack /></VerifyAdmin>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</SiteContext.Provider>
|
</SiteContext.Provider>
|
||||||
|
|
166
src/routes/AdminCreateTrack.tsx
Normal file
166
src/routes/AdminCreateTrack.tsx
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
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 song</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</FormControl.Label>
|
||||||
|
<TextInput type="time" />
|
||||||
|
</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 / 1000 / 3600 * 60;
|
||||||
|
//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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ export function AdminTrackList() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }}>Create</Button></Heading>
|
<Heading>All tracks (admin) <Button sx={{ marginBottom: 2 }} onClick={() => navigate("/admin/tracks/create")}>Create</Button></Heading>
|
||||||
<Box className="songCategory">
|
<Box className="songCategory">
|
||||||
{
|
{
|
||||||
tracks.map(x => {
|
tracks.map(x => {
|
||||||
|
|
|
@ -58,7 +58,7 @@ export function Profile() {
|
||||||
<Dialog isOpen={isActivateDialogOpen} onDismiss={() => setIsActivateDialogOpen(false)} aria-labelledby="header">
|
<Dialog isOpen={isActivateDialogOpen} onDismiss={() => setIsActivateDialogOpen(false)} aria-labelledby="header">
|
||||||
<Dialog.Header id="header">Activate song</Dialog.Header>
|
<Dialog.Header id="header">Activate song</Dialog.Header>
|
||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Text fontFamily="sans-serif">In order to activate a song for use, you need to sacrifice another song.<br />Please select a song you own to replace:</Text>
|
<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>
|
||||||
<ActionMenu.Button>Select a song...</ActionMenu.Button>
|
<ActionMenu.Button>Select a song...</ActionMenu.Button>
|
||||||
<ActionMenu.Overlay width="medium">
|
<ActionMenu.Overlay width="medium">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user