mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-03 04:18:16 +03:00
Merge pull request #695 from TNG-dev/zkldi/issue-370
This commit is contained in:
+1
-1
@@ -85,7 +85,7 @@
|
||||
"rimraf": "3.0.2",
|
||||
"safe-json-stringify": "1.2.0",
|
||||
"seq-logging": "1.1.1",
|
||||
"tachi-common": "0.8.1",
|
||||
"tachi-common": "0.9.3",
|
||||
"ts-node": "10.4.0",
|
||||
"tsconfig-paths": "3.12.0",
|
||||
"typescript": "4.5.5",
|
||||
|
||||
Generated
+10
-7
@@ -65,7 +65,7 @@ specifiers:
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: 1.1.1
|
||||
supertest: 6.2.2
|
||||
tachi-common: 0.8.1
|
||||
tachi-common: 0.9.3
|
||||
tap: 15.1.6
|
||||
ts-node: 10.4.0
|
||||
tsconfig-paths: 3.12.0
|
||||
@@ -112,7 +112,7 @@ dependencies:
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: 1.1.1
|
||||
tachi-common: 0.8.1
|
||||
tachi-common: 0.9.3
|
||||
ts-node: 10.4.0_2615db9039ce432b4abf2fc39ef336ec
|
||||
tsconfig-paths: 3.12.0
|
||||
typescript: 4.5.5
|
||||
@@ -1381,7 +1381,6 @@ packages:
|
||||
'@types/node': 17.0.15
|
||||
'@types/qs': 6.9.7
|
||||
'@types/range-parser': 1.2.4
|
||||
dev: true
|
||||
|
||||
/@types/express-session/1.17.4:
|
||||
resolution: {integrity: sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==}
|
||||
@@ -1474,11 +1473,9 @@ packages:
|
||||
|
||||
/@types/qs/6.9.7:
|
||||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
dev: true
|
||||
|
||||
/@types/range-parser/1.2.4:
|
||||
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
|
||||
dev: true
|
||||
|
||||
/@types/rate-limit-redis/1.7.4:
|
||||
resolution: {integrity: sha512-TDsh9CJHHhx+sGHlqF6MI+PgEv91o/wQ67e2IR02ebYPzWQ8Rk0pcVY+WBy2pjxa+cAkPwjcOFDd21VToKaOzQ==}
|
||||
@@ -4725,6 +4722,10 @@ packages:
|
||||
resolution: {integrity: sha512-7214GHTBThBI/PGlKWXUSJ++ca0ld54VpF4k98U1EDAFXai5mnvWbb4speVOnWkilqfN25x8biWUAj4gHNu5Og==}
|
||||
dev: false
|
||||
|
||||
/prudence/0.9.8:
|
||||
resolution: {integrity: sha512-my6HEhcTW3aKKjAx8j6jgUj3x5Kc71NEv3DySRXun2Zspw6Fdo5zDnbpXv/j4tyk9f9XXrpu1grWVBX4WU+1Tg==}
|
||||
dev: false
|
||||
|
||||
/psl/1.8.0:
|
||||
resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==}
|
||||
dev: true
|
||||
@@ -5363,10 +5364,12 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.8.1:
|
||||
resolution: {integrity: sha512-YDVxlxdli9u7MI9rhN76CUkfA/VoiXJKGyhg22uUAxMfENu9nxHWxFM3Iw20Gg+gZ8lPl3Yhvxq+qi7fD0SiNA==}
|
||||
/tachi-common/0.9.3:
|
||||
resolution: {integrity: sha512-7wM+WAfdt0EJw5Bahu5puL0Am/5Nmop68maEyHwHZXTZDQoOqREYxv9+5+Co/wvdcApTZ2T3ctWl1UaPBQyhyQ==}
|
||||
dependencies:
|
||||
'@types/express-serve-static-core': 4.17.28
|
||||
'@types/mongodb': 3.6.18
|
||||
prudence: 0.9.8
|
||||
dev: false
|
||||
|
||||
/tap-mocha-reporter/5.0.1:
|
||||
|
||||
Vendored
+1
-37
@@ -195,43 +195,7 @@ const db = {
|
||||
migrations: monkDB.get<MigrationDocument>("migrations"),
|
||||
};
|
||||
|
||||
export type StaticDatabases =
|
||||
| "sessions"
|
||||
| "session-view-cache"
|
||||
| "folders"
|
||||
| "folder-chart-lookup"
|
||||
| "scores"
|
||||
| "personal-bests"
|
||||
| "imports"
|
||||
| "import-timings"
|
||||
| "goals"
|
||||
| "goal-subs"
|
||||
| "milestone-subs"
|
||||
| "milestones"
|
||||
| "game-stats"
|
||||
| "game-settings"
|
||||
| "users"
|
||||
| "kai-auth-tokens"
|
||||
| "bms-course-lookup"
|
||||
| "api-tokens"
|
||||
| "import-locks"
|
||||
| "tables"
|
||||
| "game-stats-snapshots"
|
||||
| "arc-saved-profiles"
|
||||
| "user-private-information"
|
||||
| "api-clients"
|
||||
| "oauth2-auth-codes"
|
||||
| "fer-settings"
|
||||
| "orphan-chart-queue"
|
||||
| "password-reset-codes"
|
||||
| "user-settings"
|
||||
| "counters"
|
||||
| "score-blacklist"
|
||||
| "verify-email-codes"
|
||||
| "class-achievements"
|
||||
| "recent-folder-views"
|
||||
| "milestone-sets"
|
||||
| "migrations";
|
||||
export type StaticDatabases = Exclude<keyof typeof db, "songs" | "charts">;
|
||||
|
||||
export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`;
|
||||
|
||||
|
||||
Vendored
+16
-682
@@ -1,18 +1,7 @@
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import { AllPermissions } from "server/middleware/auth";
|
||||
import {
|
||||
Game,
|
||||
GamePTConfig,
|
||||
GetGameConfig,
|
||||
GetGamePTConfig,
|
||||
Playtypes,
|
||||
UserAuthLevels,
|
||||
} from "tachi-common";
|
||||
import { allIDStrings, allImportTypes } from "tachi-common/js/config/static-config";
|
||||
import { IsValidGame, IsValidPlaytype } from "utils/misc";
|
||||
import { optNull } from "utils/prudence";
|
||||
import { StaticDatabases } from "./db";
|
||||
import { SCHEMAS, SchemaValidatorFunction } from "tachi-common/js/lib/schemas";
|
||||
import { Databases } from "./db";
|
||||
|
||||
// The idea of this file is to export a function for every database
|
||||
// that will validate that collection.
|
||||
@@ -32,326 +21,10 @@ function prSchemaify(schema: PrudenceSchema) {
|
||||
};
|
||||
}
|
||||
|
||||
const games = TachiConfig.GAMES;
|
||||
const isValidPlaytype = (self: unknown, parent: Record<string, unknown>) => {
|
||||
if (!parent.game || typeof parent.game !== "string" || !IsValidGame(parent.game)) {
|
||||
throw new Error(`Invalid Schema, need game to base IsValidPlaytype off of.`);
|
||||
}
|
||||
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string.";
|
||||
}
|
||||
|
||||
if (!IsValidPlaytype(parent.game, self)) {
|
||||
return `Expected a valid playtype for ${parent.game}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const extractGPT = (self: unknown) => {
|
||||
if (typeof self !== "object" || !self) {
|
||||
throw new Error("Expected an object.");
|
||||
}
|
||||
|
||||
const s = self as Record<string, unknown>;
|
||||
|
||||
if (typeof s.game !== "string") {
|
||||
throw new Error(`Expected a string where self.game is. Got ${s.game}`);
|
||||
}
|
||||
|
||||
if (typeof s.playtype !== "string") {
|
||||
throw new Error(`Expected a string where self.playtype is. Got ${s.playtype}`);
|
||||
}
|
||||
|
||||
if (!IsValidGame(s.game)) {
|
||||
throw new Error(`Expected valid game -- got ${s.game}.`);
|
||||
}
|
||||
|
||||
if (!IsValidPlaytype(s.game, s.playtype)) {
|
||||
throw new Error(`Expected valid playtype -- got ${s.playtype}.`);
|
||||
}
|
||||
|
||||
return { game: s.game, playtype: s.playtype };
|
||||
};
|
||||
|
||||
export type ValidatorFunction = (s: unknown) => true;
|
||||
|
||||
const PR_GameStats = (game: Game, playtype: Playtypes[Game], gptConfig: GamePTConfig) => ({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
game: p.is(game),
|
||||
playtype: p.is(playtype),
|
||||
ratings: Object.fromEntries(gptConfig.scoreRatingAlgs.map((s) => [s, "*number"])),
|
||||
classes: Object.fromEntries(
|
||||
Object.keys(gptConfig.classHumanisedFormat).map((e) => [e, p.optional(p.isInteger)])
|
||||
),
|
||||
});
|
||||
|
||||
const PR_GoalInfo = {
|
||||
progress: "?number",
|
||||
progressHuman: "string",
|
||||
outOf: "number",
|
||||
outOfHuman: "string",
|
||||
achieved: "boolean",
|
||||
};
|
||||
|
||||
const GetHitMeta = (game: Game): PrudenceSchema => {
|
||||
if (game === "iidx") {
|
||||
return {
|
||||
bp: optNull(p.isPositiveInteger),
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
gaugeHistory: optNull([p.nullable(p.isBetween(0, 100))]),
|
||||
comboBreak: optNull(p.isPositiveInteger),
|
||||
gsm: optNull({
|
||||
EASY: [p.nullable(p.isBetween(0, 100))],
|
||||
NORMAL: [p.nullable(p.isBetween(0, 100))],
|
||||
HARD: [p.nullable(p.isBetween(0, 100))],
|
||||
EX_HARD: [p.nullable(p.isBetween(0, 100))],
|
||||
}),
|
||||
};
|
||||
} else if (game === "bms") {
|
||||
return {
|
||||
epg: optNull(p.isPositiveInteger),
|
||||
egr: optNull(p.isPositiveInteger),
|
||||
egd: optNull(p.isPositiveInteger),
|
||||
epr: optNull(p.isPositiveInteger),
|
||||
ebd: optNull(p.isPositiveInteger),
|
||||
lpg: optNull(p.isPositiveInteger),
|
||||
lgr: optNull(p.isPositiveInteger),
|
||||
lgd: optNull(p.isPositiveInteger),
|
||||
lpr: optNull(p.isPositiveInteger),
|
||||
lbd: optNull(p.isPositiveInteger),
|
||||
bp: optNull(p.isPositiveInteger),
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
};
|
||||
} else if (game === "sdvx" || game === "usc") {
|
||||
return { gauge: optNull(p.isBetween(0, 100)) };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const PR_Random = p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR");
|
||||
|
||||
const GetScoreMeta = (game: Game, playtype: Playtypes[Game]): PrudenceSchema => {
|
||||
if (game === "iidx" && playtype === "SP") {
|
||||
return {
|
||||
random: optNull(PR_Random),
|
||||
assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")),
|
||||
range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")),
|
||||
gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD")),
|
||||
};
|
||||
} else if (game === "iidx" && playtype === "DP") {
|
||||
return {
|
||||
random: optNull({
|
||||
left: PR_Random,
|
||||
right: PR_Random,
|
||||
}),
|
||||
assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")),
|
||||
range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")),
|
||||
gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD")),
|
||||
};
|
||||
} else if (game === "sdvx") {
|
||||
return { inSkillAnalyser: "*?boolean" };
|
||||
} else if (game === "usc") {
|
||||
return {
|
||||
noteMod: optNull(p.isIn("NORMAL", "MIRROR", "RANDOM", "MIR-RAN")),
|
||||
gaugeMod: optNull(p.isIn("NORMAL", "HARD", "PERMISSIVE")),
|
||||
};
|
||||
} else if (game === "bms" && playtype === "7K") {
|
||||
return {
|
||||
random: optNull(PR_Random),
|
||||
inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")),
|
||||
client: optNull(p.isIn("LR2", "lr2oraja")),
|
||||
};
|
||||
} else if (game === "bms" && playtype === "14K") {
|
||||
return {
|
||||
random: optNull({
|
||||
left: PR_Random,
|
||||
right: PR_Random,
|
||||
}),
|
||||
inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")),
|
||||
client: optNull(p.isIn("LR2", "lr2oraja")),
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export const DatabaseSchemas: Record<
|
||||
Exclude<StaticDatabases, "folders" | "tables" | "bms-course-lookup">,
|
||||
ValidatorFunction
|
||||
> = {
|
||||
users: prSchemaify({
|
||||
username: p.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u),
|
||||
usernameLowercase: (self, parent) => self === (parent.username as string).toLowerCase(),
|
||||
id: p.isPositiveInteger,
|
||||
socialMedia: {
|
||||
discord: "*?string",
|
||||
twitter: "*?string",
|
||||
github: "*?string",
|
||||
steam: "*?string",
|
||||
youtube: "*?string",
|
||||
twitch: "*?string",
|
||||
},
|
||||
joinDate: p.isPositiveInteger,
|
||||
about: p.isBoundedString(0, 4000),
|
||||
status: p.nullable(p.isBoundedString(3, 140)),
|
||||
customPfpLocation: "?string",
|
||||
customBannerLocation: "?string",
|
||||
clan: p.nullable(p.isBoundedString(2, 4)),
|
||||
lastSeen: p.isPositiveInteger,
|
||||
badges: [p.isIn("beta", "alpha", "devTeam")],
|
||||
authLevel: p.isBoundedInteger(UserAuthLevels.BANNED, UserAuthLevels.ADMIN),
|
||||
}),
|
||||
"api-tokens": prSchemaify({
|
||||
userID: p.nullable(p.isPositiveNonZeroInteger),
|
||||
token: "?string",
|
||||
identifier: "string",
|
||||
permissions: (self) => {
|
||||
if (typeof self !== "object" || !self) {
|
||||
return `Invalid permissions - expected an object.`;
|
||||
}
|
||||
|
||||
// Typescript moment...
|
||||
const s = self as Record<string, unknown>;
|
||||
|
||||
for (const key in AllPermissions) {
|
||||
if (s[key] !== undefined && typeof s[key] !== "boolean") {
|
||||
return `Invalid permission value of ${s[key]} at ${key}.`;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
fromOAuth2Client: "*string",
|
||||
}),
|
||||
"arc-saved-profiles": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
accountID: "string",
|
||||
forImportType: p.isIn("api/arc-iidx", "api/arc-sdvx"),
|
||||
}),
|
||||
"class-achievements": prSchemaify({
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
classSet: (self, parent) => {
|
||||
const gptConfig = GetGamePTConfig(
|
||||
parent.game as Game,
|
||||
parent.playtype as Playtypes[Game]
|
||||
);
|
||||
|
||||
const keys = Object.keys(gptConfig.classHumanisedFormat);
|
||||
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string.";
|
||||
}
|
||||
|
||||
if (!keys.includes(self)) {
|
||||
return `Expected any of ${keys.join(", ")}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
classOldValue: p.nullable(p.isInteger),
|
||||
classValue: p.isInteger,
|
||||
timeAchieved: p.isPositive,
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
}),
|
||||
"fer-settings": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
cards: p.nullable(
|
||||
(self) =>
|
||||
Array.isArray(self) && self.length <= 6 && self.every((t) => typeof t === "string")
|
||||
),
|
||||
forceStaticImport: "boolean",
|
||||
}),
|
||||
"folder-chart-lookup": prSchemaify({
|
||||
chartID: "string",
|
||||
folderID: "string",
|
||||
}),
|
||||
"game-settings": (s) => {
|
||||
const { game, playtype } = extractGPT(s);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
game: p.is(game),
|
||||
playtype: p.is(playtype),
|
||||
preferences: {
|
||||
preferredScoreAlg: p.nullable(p.isIn(gptConfig.scoreRatingAlgs)),
|
||||
preferredSessionAlg: p.nullable(p.isIn(gptConfig.sessionRatingAlgs)),
|
||||
preferredProfileAlg: p.nullable(p.isIn(gptConfig.profileRatingAlgs)),
|
||||
// ouch
|
||||
stats: p.and(
|
||||
[
|
||||
p.or(
|
||||
{
|
||||
mode: p.is("chart"),
|
||||
chartID: "string",
|
||||
property: p.isIn("score", "percent", "grade", "lamp", "playcount"),
|
||||
},
|
||||
{
|
||||
mode: p.is("folder"),
|
||||
folderID: p.or("string", ["string"]),
|
||||
property: p.isIn("score", "percent", "grade", "lamp"),
|
||||
gte: p.isPositive,
|
||||
}
|
||||
),
|
||||
],
|
||||
(self) => Array.isArray(self) && self.length <= 6
|
||||
),
|
||||
scoreBucket: p.isIn(null, "grade", "lamp"),
|
||||
gameSpecific:
|
||||
game === "iidx"
|
||||
? {
|
||||
display2DXTra: "boolean",
|
||||
}
|
||||
: {},
|
||||
},
|
||||
})(s);
|
||||
},
|
||||
"game-stats": (self) => {
|
||||
const { game, playtype } = extractGPT(self);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return prSchemaify(PR_GameStats(game, playtype, gptConfig))(self);
|
||||
},
|
||||
"game-stats-snapshots": (self) => {
|
||||
const { game, playtype } = extractGPT(self);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return prSchemaify(
|
||||
Object.assign(PR_GameStats(game, playtype, gptConfig), {
|
||||
rankings: Object.fromEntries(
|
||||
gptConfig.profileRatingAlgs.map((e) => [
|
||||
e,
|
||||
{ outOf: p.isPositiveNonZeroInteger, ranking: p.isPositiveNonZeroInteger },
|
||||
])
|
||||
),
|
||||
playcount: p.isPositiveInteger,
|
||||
timestamp: p.isPositive,
|
||||
})
|
||||
)(self);
|
||||
},
|
||||
export const DatabaseSchemas: Record<Databases, SchemaValidatorFunction> = {
|
||||
"import-locks": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
}),
|
||||
"import-timings": prSchemaify({
|
||||
importID: "string",
|
||||
timestamp: p.isPositive,
|
||||
total: p.isPositive,
|
||||
rel: Object.fromEntries(
|
||||
["import", "importParse", "session", "pb"].map((k) => [k, p.isPositive])
|
||||
),
|
||||
abs: Object.fromEntries(
|
||||
["parse", "import", "importParse", "session", "pb", "ugs", "goal", "milestone"].map(
|
||||
(k) => [k, p.isPositive]
|
||||
)
|
||||
),
|
||||
}),
|
||||
"kai-auth-tokens": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
token: "string",
|
||||
@@ -363,368 +36,16 @@ export const DatabaseSchemas: Record<
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
createdOn: p.isPositive,
|
||||
}),
|
||||
"api-clients": prSchemaify({
|
||||
clientID: "string",
|
||||
clientSecret: "string",
|
||||
name: "string",
|
||||
author: p.isPositiveNonZeroInteger,
|
||||
requestedPermissions: [p.isIn(Object.keys(AllPermissions))],
|
||||
redirectUri: "?string",
|
||||
webhookUri: "?string",
|
||||
apiKeyTemplate: p.nullable((self) => {
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string.";
|
||||
}
|
||||
|
||||
if (!self.includes("%%TACHI_KEY%%")) {
|
||||
return "Template must include %%TACHI_KEY%%.";
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
apiKeyFilename: "?string",
|
||||
}),
|
||||
"orphan-chart-queue": prSchemaify({
|
||||
idString: p.isIn(allIDStrings),
|
||||
// @todo #370 Properly prudence-def orphan-chart-queue.
|
||||
chartDoc: p.any,
|
||||
songDoc: p.any,
|
||||
userIDs: [p.isPositiveNonZeroInteger],
|
||||
}),
|
||||
"password-reset-codes": prSchemaify({
|
||||
code: "string",
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
createdOn: p.isPositive,
|
||||
}),
|
||||
"score-blacklist": prSchemaify({
|
||||
scoreID: "string",
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
score: p.any,
|
||||
}),
|
||||
"session-view-cache": prSchemaify({
|
||||
sessionID: "string",
|
||||
ip: "string",
|
||||
timestamp: p.isPositive,
|
||||
}),
|
||||
"user-private-information": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
password: "string",
|
||||
email: "string",
|
||||
}),
|
||||
"verify-email-codes": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
code: "string",
|
||||
email: "string",
|
||||
}),
|
||||
counters: prSchemaify({
|
||||
counterName: "string",
|
||||
value: p.isPositiveInteger,
|
||||
}),
|
||||
"user-settings": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
preferences: {
|
||||
invisible: "boolean",
|
||||
developerMode: "boolean",
|
||||
advancedMode: "boolean",
|
||||
contentiousContent: "boolean",
|
||||
deletableScores: "boolean",
|
||||
},
|
||||
}),
|
||||
sessions: (self) => {
|
||||
const { game, playtype } = extractGPT(self);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
sessionID: "string",
|
||||
name: "string",
|
||||
desc: "?string",
|
||||
game: p.is(game),
|
||||
playtype: isValidPlaytype,
|
||||
importType: p.nullable(p.isIn(allImportTypes)),
|
||||
timeInserted: p.isPositiveInteger,
|
||||
timeEnded: p.isPositiveInteger,
|
||||
timeStarted: p.isPositiveInteger,
|
||||
highlight: "boolean",
|
||||
views: p.isInteger,
|
||||
calculatedData: Object.fromEntries(
|
||||
gptConfig.sessionRatingAlgs.map((k) => [k, "*?number"])
|
||||
),
|
||||
scoreInfo: [
|
||||
p.or(
|
||||
{
|
||||
scoreID: "string",
|
||||
isNewScore: p.is(true),
|
||||
},
|
||||
{
|
||||
scoreID: "string",
|
||||
isNewScore: p.is(false),
|
||||
scoreDelta: "number",
|
||||
gradeDelta: p.isInteger,
|
||||
lampDelta: p.isInteger,
|
||||
percentDelta: "number",
|
||||
}
|
||||
),
|
||||
],
|
||||
})(self);
|
||||
},
|
||||
imports: prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
timeStarted: p.isPositive,
|
||||
timeFinished: p.isPositive,
|
||||
idStrings: [p.isIn(allIDStrings)],
|
||||
importID: "string",
|
||||
scoreIDs: ["string"],
|
||||
game: p.isIn(games),
|
||||
// @ts-expect-error We've asserted this is definitely a game.
|
||||
playtypes: [(self) => p.isIn(GetGameConfig(self.game).validPlaytypes)(self)],
|
||||
errors: [
|
||||
{
|
||||
type: "string",
|
||||
message: "?string",
|
||||
},
|
||||
],
|
||||
createdSessions: [
|
||||
{
|
||||
sessionID: "string",
|
||||
type: p.isIn("Created", "Appended"),
|
||||
},
|
||||
],
|
||||
importType: p.nullable(p.isIn(allImportTypes)),
|
||||
classDeltas: [
|
||||
{
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
set: "string", // Technically false, but the validation is a pain.
|
||||
old: p.nullable(p.isInteger),
|
||||
new: p.isInteger,
|
||||
},
|
||||
],
|
||||
goalInfo: [
|
||||
{
|
||||
goalID: "string",
|
||||
old: PR_GoalInfo,
|
||||
new: PR_GoalInfo,
|
||||
},
|
||||
],
|
||||
milestoneInfo: [
|
||||
{
|
||||
milestoneID: "string",
|
||||
old: { progress: p.isInteger, achieved: "boolean" },
|
||||
new: { progress: p.isInteger, achieved: "boolean" },
|
||||
},
|
||||
],
|
||||
userIntent: "boolean",
|
||||
}),
|
||||
scores: (self) => {
|
||||
const { game, playtype } = extractGPT(self);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const hitMeta = Object.assign(
|
||||
{
|
||||
fast: optNull(p.isPositiveInteger),
|
||||
slow: optNull(p.isPositiveInteger),
|
||||
maxCombo: optNull(p.isPositiveInteger),
|
||||
},
|
||||
GetHitMeta(game)
|
||||
);
|
||||
|
||||
const scoreMeta = GetScoreMeta(game, playtype);
|
||||
|
||||
return prSchemaify({
|
||||
service: "string",
|
||||
game: p.is(game),
|
||||
playtype: p.is(playtype),
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
songID: p.isPositiveInteger,
|
||||
chartID: "string",
|
||||
isPrimary: "boolean",
|
||||
highlight: "boolean",
|
||||
comment: "?string",
|
||||
timeAdded: p.isInteger,
|
||||
scoreID: "string",
|
||||
importType: p.nullable(p.isIn(allImportTypes)),
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
scoreData: {
|
||||
score: "number",
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
lampIndex: p.isIn(Object.keys(gptConfig.lamps).map(Number)),
|
||||
grade: p.isIn(gptConfig.grades),
|
||||
gradeIndex: p.isIn(Object.keys(gptConfig.grades).map(Number)),
|
||||
percent: p.isBetween(0, gptConfig.percentMax),
|
||||
esd: "?number",
|
||||
judgements: Object.fromEntries(
|
||||
gptConfig.judgements.map((j) => [j, optNull(p.isInteger)])
|
||||
),
|
||||
hitMeta,
|
||||
},
|
||||
scoreMeta,
|
||||
calculatedData: Object.fromEntries(
|
||||
gptConfig.scoreRatingAlgs.map((a) => [a, "*?number"])
|
||||
),
|
||||
})(self);
|
||||
},
|
||||
"personal-bests": (self) => {
|
||||
const { game, playtype } = extractGPT(self);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const hitMeta = Object.assign(
|
||||
{
|
||||
fast: optNull(p.isPositiveInteger),
|
||||
slow: optNull(p.isPositiveInteger),
|
||||
maxCombo: optNull(p.isPositiveInteger),
|
||||
},
|
||||
GetHitMeta(game)
|
||||
);
|
||||
|
||||
return prSchemaify({
|
||||
composedFrom: {
|
||||
scorePB: "string",
|
||||
lampPB: "string",
|
||||
other: p.optional([{ name: "string", scoreID: "string" }]),
|
||||
},
|
||||
rankingData: {
|
||||
rank: p.isPositiveNonZeroInteger,
|
||||
outOf: p.isPositiveNonZeroInteger,
|
||||
},
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
chartID: "string",
|
||||
game: p.is(game),
|
||||
playtype: p.is(playtype),
|
||||
songID: p.isPositiveNonZeroInteger,
|
||||
highlight: "boolean",
|
||||
isPrimary: "boolean",
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
scoreData: {
|
||||
score: "number",
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
lampIndex: p.isIn(Object.keys(gptConfig.lamps).map(Number)),
|
||||
grade: p.isIn(gptConfig.grades),
|
||||
gradeIndex: p.isIn(Object.keys(gptConfig.grades).map(Number)),
|
||||
percent: p.isBetween(0, gptConfig.percentMax),
|
||||
esd: "?number",
|
||||
judgements: Object.fromEntries(
|
||||
gptConfig.judgements.map((j) => [j, optNull(p.isInteger)])
|
||||
),
|
||||
hitMeta,
|
||||
},
|
||||
calculatedData: Object.fromEntries(
|
||||
gptConfig.scoreRatingAlgs.map((a) => [a, "*?number"])
|
||||
),
|
||||
})(self);
|
||||
},
|
||||
goals: prSchemaify({
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
timeAdded: p.isPositive,
|
||||
title: "string",
|
||||
goalID: "string",
|
||||
criteria: p.or(
|
||||
{
|
||||
mode: p.is("single"),
|
||||
key: p.isIn(
|
||||
"scoreData.percent",
|
||||
"scoreData.lampIndex",
|
||||
"scoreData.gradeIndex",
|
||||
"scoreData.score"
|
||||
),
|
||||
value: "number",
|
||||
},
|
||||
{
|
||||
mode: p.isIn("absolute", "proportion"),
|
||||
countNum: p.isPositive,
|
||||
key: p.isIn(
|
||||
"scoreData.percent",
|
||||
"scoreData.lampIndex",
|
||||
"scoreData.gradeIndex",
|
||||
"scoreData.score"
|
||||
),
|
||||
value: "number",
|
||||
}
|
||||
),
|
||||
charts: p.or(
|
||||
{
|
||||
type: p.is("any"),
|
||||
},
|
||||
{
|
||||
type: p.is("folder"),
|
||||
data: "string",
|
||||
},
|
||||
{
|
||||
type: p.is("multi"),
|
||||
data: ["string"],
|
||||
},
|
||||
{
|
||||
type: p.is("single"),
|
||||
data: "string",
|
||||
}
|
||||
),
|
||||
}),
|
||||
milestones: prSchemaify({
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
name: "string",
|
||||
desc: "string",
|
||||
milestoneID: "string",
|
||||
group: "?string",
|
||||
groupINdex: p.nullable(p.isInteger),
|
||||
milestoneData: [
|
||||
{
|
||||
title: "string",
|
||||
desc: "string",
|
||||
goals: [{ goalID: "string", note: "*string" }],
|
||||
},
|
||||
],
|
||||
criteria: p.or(
|
||||
{
|
||||
type: "all",
|
||||
},
|
||||
{
|
||||
type: p.isIn("absolute", "proportion"),
|
||||
value: p.isPositive,
|
||||
}
|
||||
),
|
||||
}),
|
||||
"goal-subs": prSchemaify({
|
||||
goalID: "string",
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
achieved: "boolean",
|
||||
timeSet: p.isPositive,
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
lastInteraction: p.nullable(p.isPositive),
|
||||
progress: "?number",
|
||||
progressHuman: "string",
|
||||
outOf: "number",
|
||||
outOfHuman: "string",
|
||||
}),
|
||||
"milestone-subs": prSchemaify({
|
||||
milestoneID: "string",
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
timeSet: p.isPositive,
|
||||
achieved: "boolean",
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
progress: p.isInteger,
|
||||
}),
|
||||
"recent-folder-views": prSchemaify({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
folderID: "string",
|
||||
lastViewed: "number",
|
||||
}),
|
||||
"milestone-sets": prSchemaify({
|
||||
setID: "string",
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
milestones: ["string"],
|
||||
}),
|
||||
migrations: prSchemaify({
|
||||
migrationID: "string",
|
||||
status: p.isIn("applied", "pending"),
|
||||
@@ -739,4 +60,17 @@ export const DatabaseSchemas: Record<
|
||||
return p.isPositiveInteger(self);
|
||||
},
|
||||
}),
|
||||
"orphan-scores": prSchemaify({
|
||||
importType: p.isIn(TachiConfig.IMPORT_TYPES),
|
||||
game: p.isIn(TachiConfig.GAMES),
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
orphanID: "string",
|
||||
timeInserted: p.isPositive,
|
||||
errMsg: "?string",
|
||||
// @todo Type these properly? This will take a lot of effort, as it will require
|
||||
// formal types for all parser data:context duos.
|
||||
data: p.any,
|
||||
context: p.any,
|
||||
}),
|
||||
...SCHEMAS,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import expMiddlewareMock from "express-request-mock";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
import { ALL_PERMISSIONS } from "tachi-common";
|
||||
import t from "tap";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { AllPermissions, SetRequestPermissions } from "./auth";
|
||||
import { SetRequestPermissions } from "./auth";
|
||||
|
||||
t.test("#SetRequestPermissions", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
@@ -112,7 +113,7 @@ t.test("#SetRequestPermissions", (t) => {
|
||||
userID: 1,
|
||||
identifier: `Session-Key 1`,
|
||||
token: null,
|
||||
permissions: AllPermissions,
|
||||
permissions: ALL_PERMISSIONS,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
t.end();
|
||||
|
||||
@@ -3,7 +3,7 @@ import db from "external/mongo/db";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { APIPermissions, APITokenDocument, UserAuthLevels } from "tachi-common";
|
||||
import { APIPermissions, APITokenDocument, UserAuthLevels, ALL_PERMISSIONS } from "tachi-common";
|
||||
import { SplitAuthorizationHeader } from "utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -16,16 +16,6 @@ const GuestToken: APITokenDocument = {
|
||||
fromAPIClient: null,
|
||||
};
|
||||
|
||||
export const AllPermissions: Record<APIPermissions, true> = {
|
||||
customise_profile: true,
|
||||
submit_score: true,
|
||||
customise_session: true,
|
||||
customise_score: true,
|
||||
delete_score: true,
|
||||
manage_targets: true,
|
||||
manage_rivals: true,
|
||||
};
|
||||
|
||||
export const SetRequestPermissions: RequestHandler = CreateSetRequestPermissions("description");
|
||||
|
||||
/**
|
||||
@@ -49,7 +39,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler {
|
||||
userID: req.session.tachi.user.id,
|
||||
identifier: `Session-Key ${req.session.tachi.user.id}`,
|
||||
token: null,
|
||||
permissions: AllPermissions,
|
||||
permissions: ALL_PERMISSIONS,
|
||||
fromAPIClient: null,
|
||||
};
|
||||
return next();
|
||||
|
||||
@@ -4,9 +4,13 @@ import { SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import p from "prudence";
|
||||
import { AllPermissions } from "server/middleware/auth";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { APIPermissions, TachiAPIClientDocument, UserAuthLevels } from "tachi-common";
|
||||
import {
|
||||
ALL_PERMISSIONS,
|
||||
APIPermissions,
|
||||
TachiAPIClientDocument,
|
||||
UserAuthLevels,
|
||||
} from "tachi-common";
|
||||
import { DedupeArr, DeleteUndefinedProps, IsValidURL, Random20Hex } from "utils/misc";
|
||||
import { optNull } from "utils/prudence";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
@@ -79,7 +83,7 @@ router.post(
|
||||
return true;
|
||||
},
|
||||
apiKeyFilename: "?string",
|
||||
permissions: [p.isIn(Object.keys(AllPermissions))],
|
||||
permissions: [p.isIn(Object.keys(ALL_PERMISSIONS))],
|
||||
}),
|
||||
async (req, res) => {
|
||||
if (!req.session.tachi?.user) {
|
||||
|
||||
@@ -3,9 +3,8 @@ import db from "external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import p from "prudence";
|
||||
import { AllPermissions } from "server/middleware/auth";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { APIPermissions, APITokenDocument } from "tachi-common";
|
||||
import { ALL_PERMISSIONS, APIPermissions, APITokenDocument } from "tachi-common";
|
||||
import { Random20Hex } from "utils/misc";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
import { RequireSelfRequestFromUser } from "../middleware";
|
||||
@@ -49,7 +48,7 @@ router.get("/", async (req, res) => {
|
||||
router.post(
|
||||
"/create",
|
||||
prValidate({
|
||||
permissions: p.optional([p.isIn(Object.keys(AllPermissions))]),
|
||||
permissions: p.optional([p.isIn(Object.keys(ALL_PERMISSIONS))]),
|
||||
identifier: "*string",
|
||||
clientID: "*string",
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ClearTestingRateLimitCache } from "server/middleware/rate-limiter";
|
||||
import supertest from "supertest";
|
||||
import { AllPermissions } from "../server/middleware/auth";
|
||||
import { ALL_PERMISSIONS } from "tachi-common";
|
||||
import ResetDBState from "./resets";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -33,7 +33,7 @@ export function InsertFakeTokenWithAllPerms(token: string): () => any {
|
||||
await db["api-tokens"].insert({
|
||||
userID: 1,
|
||||
identifier: "Mock API Token",
|
||||
permissions: AllPermissions,
|
||||
permissions: ALL_PERMISSIONS,
|
||||
token,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user