mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-08 06:48:04 +03:00
Massive Update to sync with client
This commit is contained in:
Vendored
+11
-11
@@ -1,17 +1,6 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "buildrun",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: buildrun",
|
||||
"detail": "tsc && export NODE_ENV=\"dev\" && node js/main.js",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "fulltest",
|
||||
@@ -37,6 +26,17 @@
|
||||
"focus": false,
|
||||
"panel": "dedicated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: start",
|
||||
"detail": "tsc --project tsconfig.build.json && node js/main.js",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
"watchtest": "tap --watch",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"lint": "eslint ./src --ext .ts --fix",
|
||||
"buildrun": "tsc --project tsconfig.build.json && node js/main.js"
|
||||
"start": "tsc --project tsconfig.build.json && node js/main.js"
|
||||
},
|
||||
"author": "zkldi",
|
||||
"license": "AGPL3",
|
||||
|
||||
Generated
+4
-4
@@ -80,7 +80,7 @@ dependencies:
|
||||
rate-limit-redis: 2.1.0
|
||||
redis: 3.1.2
|
||||
rimraf: 3.0.2
|
||||
tachi-common: github.com/zkldi/tachi-common/20ff262e31119c918684141bf82d92076425a77d_ts-node@10.0.0+typescript@4.3.4
|
||||
tachi-common: github.com/zkldi/tachi-common/7f0ff9c3905018070c291ae07f3f937d142f925d_ts-node@10.0.0+typescript@4.3.4
|
||||
typescript: 4.3.4
|
||||
winston: 3.3.3
|
||||
|
||||
@@ -4204,9 +4204,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
github.com/zkldi/tachi-common/20ff262e31119c918684141bf82d92076425a77d_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/20ff262e31119c918684141bf82d92076425a77d}
|
||||
id: github.com/zkldi/tachi-common/20ff262e31119c918684141bf82d92076425a77d
|
||||
github.com/zkldi/tachi-common/7f0ff9c3905018070c291ae07f3f937d142f925d_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/7f0ff9c3905018070c291ae07f3f937d142f925d}
|
||||
id: github.com/zkldi/tachi-common/7f0ff9c3905018070c291ae07f3f937d142f925d
|
||||
name: tachi-common
|
||||
version: 0.1.0
|
||||
dependencies:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import db from "../../src/external/mongo/db";
|
||||
import { Command } from "commander";
|
||||
import CreateLogCtx from "../../src/lib/logger/logger";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.option("-c, --code <code>", "The code for this invite.");
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
db.invites
|
||||
.insert({
|
||||
code: options.code,
|
||||
createdBy: 1,
|
||||
consumed: false,
|
||||
createdOn: Date.now(),
|
||||
})
|
||||
.then(() => {
|
||||
logger.info(`Created invite ${options.code}.`);
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { CounterDocument } from "tachi-common";
|
||||
import db from "../../src/external/mongo/db";
|
||||
|
||||
const Counters: CounterDocument[] = [
|
||||
{
|
||||
counterName: "users",
|
||||
value: 1,
|
||||
},
|
||||
];
|
||||
|
||||
db.counters.insert(Counters).then(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -27,7 +27,7 @@ t.test("#CreatePBDoc", (t) => {
|
||||
chartID,
|
||||
userID: 1,
|
||||
songID: 1,
|
||||
// rankingInfo -- is not present because it is not added until post-processing.
|
||||
// rankingData -- is not present because it is not added until post-processing.
|
||||
highlight: false,
|
||||
isPrimary: true,
|
||||
timeAchieved: 1619454485988,
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function UpdateChartRanking(chartID: string) {
|
||||
filter: { chartID: score.chartID, userID: score.userID },
|
||||
update: {
|
||||
$set: {
|
||||
rankingInfo: {
|
||||
rankingData: {
|
||||
rank,
|
||||
outOf: scores.length,
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function ProcessPBs(
|
||||
}
|
||||
|
||||
// so here's the kinda awkward part - for the time between this operation
|
||||
// and the next one - THE SCORE PBS ARE IN THE DATABASE WITHOUT RANKINGINFO.
|
||||
// and the next one - THE SCORE PBS ARE IN THE DATABASE WITHOUT RANKINGDATA.
|
||||
// this *is* bad behaviour, but I don't have a nice way to fix it.
|
||||
// This should be fixed in the future to avoid crashes between these two
|
||||
// calls - but that is unlikely.
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface TachiConfig {
|
||||
CLIENT_INDEX_HTML_PATH: string;
|
||||
ENABLE_SERVER_HTTPS: boolean;
|
||||
RUN_OWN_CDN: boolean;
|
||||
CLIENT_DEV_SERVER?: string | null;
|
||||
TYPE_INFO: StaticConfig.ServerConfig;
|
||||
}
|
||||
|
||||
@@ -71,6 +72,7 @@ const err = p(config, {
|
||||
CLIENT_INDEX_HTML_PATH: "string",
|
||||
ENABLE_SERVER_HTTPS: "boolean",
|
||||
RUN_OWN_CDN: "boolean",
|
||||
CLIENT_DEV_SERVER: "*?string",
|
||||
TYPE: p.isIn("ktchi", "btchi", "omni"),
|
||||
});
|
||||
|
||||
@@ -112,3 +114,4 @@ export const CONFIG = tachiConfig;
|
||||
export const CLIENT_INDEX_HTML_PATH = tachiConfig.CLIENT_INDEX_HTML_PATH;
|
||||
export const ENABLE_SERVER_HTTPS = tachiConfig.ENABLE_SERVER_HTTPS;
|
||||
export const RUN_OWN_CDN = tachiConfig.RUN_OWN_CDN;
|
||||
export const CLIENT_DEV_SERVER = tachiConfig.CLIENT_DEV_SERVER;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import t from "tap";
|
||||
import db from "../../../../../external/mongo/db";
|
||||
import { CloseAllConnections } from "../../../../../test-utils/close-connections";
|
||||
import mockApi from "../../../../../test-utils/mock-api";
|
||||
import ResetDBState from "../../../../../test-utils/resets";
|
||||
@@ -111,4 +112,160 @@ t.test("POST /api/v1/auth/login", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("POST /api/v1/auth/register", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() =>
|
||||
db.invites.insert({ code: "code", createdBy: 1, createdOn: 0, consumed: false })
|
||||
);
|
||||
|
||||
t.test("Should register a new user.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "foo",
|
||||
password: "password",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
t.equal(res.body.success, true);
|
||||
t.equal(res.body.body.username, "foo");
|
||||
|
||||
const doc = await db.users.findOne({ username: "foo" });
|
||||
|
||||
t.not(doc, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should disallow users with matching names.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "test_zkldi",
|
||||
password: "password",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 409);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should disallow users with matching names case insensitively.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "test_zKLdi",
|
||||
password: "password",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 409);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should disallow email if it is already used.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "foo",
|
||||
password: "password",
|
||||
email: "thepasswordis@password.com", // this is our test docs email, apparently.
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 409);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should disallow invalid emails.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "foo",
|
||||
password: "password",
|
||||
email: "nonsense+email",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should disallow short passwords.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "foo",
|
||||
password: "pass",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should disallow invalid usernames.", async (t) => {
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "3foo",
|
||||
password: "password",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
const res2 = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "f",
|
||||
password: "password",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res2.statusCode, 400);
|
||||
t.equal(res2.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should recover from a fatal error without breaking state.", async (t) => {
|
||||
await db.counters.update({ counterName: "users" }, { $set: { value: 1 } }); // this will cause a userID collision
|
||||
|
||||
const res = await mockApi.post("/api/v1/auth/register").send({
|
||||
username: "foo",
|
||||
password: "password",
|
||||
email: "foo@bar.com",
|
||||
captcha: "1",
|
||||
inviteCode: "code",
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 500);
|
||||
|
||||
const counter = await db.counters.findOne({ counterName: "users" });
|
||||
|
||||
// value should not stay incremented
|
||||
t.equal(counter?.value, 1);
|
||||
|
||||
const invite = await db.invites.findOne({ code: "code" });
|
||||
|
||||
// invite should not be consumed
|
||||
t.equal(invite?.consumed, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -10,11 +10,14 @@ import {
|
||||
import {
|
||||
FormatUserDoc,
|
||||
GetUserCaseInsensitive,
|
||||
GetUserWithEmail,
|
||||
GetUserWithID,
|
||||
PRIVATEINFO_GetUserCaseInsensitive,
|
||||
} from "../../../../../utils/user";
|
||||
import db from "../../../../../external/mongo/db";
|
||||
import CreateLogCtx from "../../../../../lib/logger/logger";
|
||||
import prValidate from "../../../../middleware/prudence-validate";
|
||||
import { DecrementCounterValue } from "../../../../../utils/db";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -126,7 +129,7 @@ router.post(
|
||||
},
|
||||
{
|
||||
username:
|
||||
"Usernames must be between 3 and 20 characters long, and can only contain alphanumeric characters!",
|
||||
"Usernames must be between 3 and 20 characters long, can only contain alphanumeric characters and cannot start with a number.",
|
||||
email: "Invalid email.",
|
||||
inviteCode: "Invalid invite code.",
|
||||
captcha: "Please fill out the captcha.",
|
||||
@@ -151,9 +154,9 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Captcha validated.");
|
||||
logger.verbose("Captcha validated.");
|
||||
} else {
|
||||
logger.info("Skipped captcha check because not in production.");
|
||||
logger.warn("Skipped captcha check because not in production.");
|
||||
}
|
||||
|
||||
const existingUser = await GetUserCaseInsensitive(req.body.username);
|
||||
@@ -166,6 +169,16 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const existingEmail = await GetUserWithEmail(req.body.email);
|
||||
|
||||
if (existingEmail) {
|
||||
logger.info(`User attempted to use email ${req.body.email}, but was already in use.`);
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
description: `This email is already in use.`,
|
||||
});
|
||||
}
|
||||
|
||||
const inviteCodeDoc = await db.invites.findOneAndUpdate(
|
||||
{
|
||||
code: req.body.inviteCode,
|
||||
@@ -197,13 +210,21 @@ router.post(
|
||||
throw new Error("AddNewUser failed to create a user.");
|
||||
}
|
||||
|
||||
// also set this as a cookie.
|
||||
req.session.tachi = {
|
||||
userID: newUser.id,
|
||||
};
|
||||
|
||||
req.session.cookie.maxAge = 3.154e10;
|
||||
req.session.cookie.secure = true;
|
||||
|
||||
// re-fetch the user like this so we guaranteeably omit the private fields.
|
||||
const user = await GetUserWithID(newUser.id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Successfully created account ${req.body.username}!`,
|
||||
body: {
|
||||
id: newUser.id,
|
||||
username: newUser.username,
|
||||
},
|
||||
body: user,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
@@ -212,6 +233,7 @@ router.post(
|
||||
);
|
||||
|
||||
await ReinstateInvite(inviteCodeDoc);
|
||||
await DecrementCounterValue("users");
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from "express";
|
||||
import { SYMBOL_TachiData } from "../../../../../../../lib/constants/tachi";
|
||||
import { FormatGPT, IsString } from "../../../../../../../utils/misc";
|
||||
import { GetGamePTConfig, UserGameStats, integer } from "tachi-common";
|
||||
import { IsString } from "../../../../../../../utils/misc";
|
||||
import { GetGamePTConfig, UserGameStats, integer, FormatGame } from "tachi-common";
|
||||
import { FindOptions } from "monk";
|
||||
import db from "../../../../../../../external/mongo/db";
|
||||
import { ParseStrPositiveInt, CheckStrProfileAlg } from "../../../../../../../utils/string-checks";
|
||||
@@ -11,7 +11,6 @@ import songIDRouter from "./songs/_songID/router";
|
||||
import { ValidatePlaytypeFromParam } from "./middleware";
|
||||
import foldersRouter from "./folders/router";
|
||||
import tablesRouter from "./tables/router";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use(ValidatePlaytypeFromParam);
|
||||
@@ -27,7 +26,7 @@ router.get("/", (req, res) => {
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved information about ${FormatGPT(game, playtype)}`,
|
||||
description: `Retrieved information about ${FormatGame(game, playtype)}`,
|
||||
body: {
|
||||
config: GetGamePTConfig(game, playtype),
|
||||
},
|
||||
|
||||
@@ -94,7 +94,7 @@ router.get("/best", async (req, res) => {
|
||||
success: true,
|
||||
description: `Retrieved ${pbs.length} personal bests.`,
|
||||
body: {
|
||||
scores: pbs,
|
||||
pbs,
|
||||
songs,
|
||||
charts,
|
||||
},
|
||||
|
||||
+47
@@ -134,4 +134,51 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/highlighted", (
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/recent", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return the users most recent sessions.", async (t) => {
|
||||
await db.sessions.remove({});
|
||||
await db.sessions.insert([
|
||||
{
|
||||
highlight: false,
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
sessionID: "recent_id1",
|
||||
timeEnded: 1234,
|
||||
},
|
||||
{
|
||||
highlight: false,
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
sessionID: "recent_id2",
|
||||
timeEnded: 12345,
|
||||
},
|
||||
{
|
||||
highlight: false,
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
sessionID: "recent_id3",
|
||||
timeEnded: 12344,
|
||||
},
|
||||
] as SessionDocument[]);
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/sessions/recent");
|
||||
|
||||
t.equal(res.body.body.length, 3);
|
||||
t.strictSame(
|
||||
// @ts-expect-error temporary type hack
|
||||
res.body.body.map((e) => e.sessionID),
|
||||
["recent_id1", "recent_id3", "recent_id2"]
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
+22
@@ -89,4 +89,26 @@ router.get("/highlighted", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a users 100 most recent sessions. Returned in timeEnded order.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/sessions/recent
|
||||
*/
|
||||
router.get("/recent", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
const sessions = await db.sessions.find(
|
||||
{ userID: user.id, game, playtype },
|
||||
{ sort: { timeEnded: -1 }, limit: 100 }
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Returned ${sessions.length} sessions.`,
|
||||
body: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -15,7 +15,7 @@ export const GetUserFromParam: RequestHandler = async (req, res, next) => {
|
||||
|
||||
if (req.params.userID === "me") {
|
||||
if (!req[SYMBOL_TachiAPIAuth].userID) {
|
||||
return res.status(403).json({
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
description: "Cannot use 'me' userID with no authentication.",
|
||||
});
|
||||
|
||||
@@ -5,8 +5,10 @@ import { integer } from "tachi-common";
|
||||
import { RedisClient } from "../external/redis/redis";
|
||||
import {
|
||||
CDN_FILE_ROOT,
|
||||
CLIENT_DEV_SERVER,
|
||||
CLIENT_INDEX_HTML_PATH,
|
||||
CONFIG,
|
||||
ENABLE_SERVER_HTTPS,
|
||||
RUN_OWN_CDN,
|
||||
SESSION_SECRET,
|
||||
} from "../lib/setup/config";
|
||||
@@ -20,6 +22,7 @@ const logger = CreateLogCtx(__filename);
|
||||
let store;
|
||||
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
logger.info("Connecting ExpressSession to Redis.");
|
||||
const RedisStore = connectRedis(expressSession);
|
||||
store = new RedisStore({
|
||||
host: "localhost",
|
||||
@@ -38,13 +41,33 @@ const userSessionMiddleware = expressSession({
|
||||
resave: true,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
secure: process.env.NODE_ENV === "production" || ENABLE_SERVER_HTTPS,
|
||||
},
|
||||
});
|
||||
|
||||
const app: Express = express();
|
||||
|
||||
app.use(helmet());
|
||||
if (process.env.NODE_ENV !== "production" && CLIENT_DEV_SERVER) {
|
||||
logger.warn(`Enabling CORS requests from ${CLIENT_DEV_SERVER}.`);
|
||||
|
||||
// Allow CORS requests from another server (since we have our dev server hosted separately).
|
||||
app.use((req, res, next) => {
|
||||
res.header("Access-Control-Allow-Origin", CLIENT_DEV_SERVER!);
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept"
|
||||
);
|
||||
res.header("Access-Control-Allow-Credentials", "true");
|
||||
res.header("Access-Control-Allow-Methods", "GET,POST,PATCH,PUT,DELETE,OPTIONS");
|
||||
next();
|
||||
});
|
||||
|
||||
// hack to allow all OPTIONS requests. Remember that this setting should not be on in production!
|
||||
// app.options("*", (req, res) => res.send());
|
||||
} else {
|
||||
logger.info("Enabling Helmet, as no CLIENT_DEV_SERVER was set, or we are in production.");
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
app.use(userSessionMiddleware);
|
||||
|
||||
@@ -77,6 +100,7 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
import mainRouter from "./router/router";
|
||||
import { SYMBOL_TachiAPIAuth } from "../lib/constants/tachi";
|
||||
|
||||
app.use("/", mainRouter);
|
||||
|
||||
@@ -88,9 +112,12 @@ if (RUN_OWN_CDN) {
|
||||
logger.warn(
|
||||
`Running OWN_CDN in production. Consider making a separate process handle your CDN for performance.`
|
||||
);
|
||||
} else {
|
||||
logger.info("Running own CDN.");
|
||||
}
|
||||
|
||||
app.use("/cdn", express.static(CDN_FILE_ROOT));
|
||||
app.get("/cdn/*", (req, res) => res.status(404).send("No content here."));
|
||||
}
|
||||
|
||||
const indexHTMLContent = fs.readFileSync(CLIENT_INDEX_HTML_PATH);
|
||||
@@ -122,6 +149,10 @@ const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (err instanceof SyntaxError) {
|
||||
const expErr: ExpressJSONErr = err as ExpressJSONErr;
|
||||
if (expErr.status === 400 && "body" in expErr) {
|
||||
logger.info(`JSON Parsing Error?`, {
|
||||
url: req.originalUrl,
|
||||
userID: req[SYMBOL_TachiAPIAuth]?.userID,
|
||||
});
|
||||
return res.status(400).send({ success: false, description: err.message });
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
{
|
||||
"counterName": "real-counter",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"counterName": "users",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetNextCounterValue } from "./db";
|
||||
import { DecrementCounterValue, GetNextCounterValue } from "./db";
|
||||
import t from "tap";
|
||||
import db from "../external/mongo/db";
|
||||
import ResetDBState from "../test-utils/resets";
|
||||
@@ -28,4 +28,29 @@ t.test("#GetNextCounterValue", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#DecrementCounterValue", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should decrease a counter.", async (t) => {
|
||||
await db.counters.insert({ counterName: "foo", value: 3 });
|
||||
|
||||
const res = await DecrementCounterValue("foo");
|
||||
|
||||
t.equal(res, 2);
|
||||
|
||||
const doc = await db.counters.findOne({ counterName: "foo" });
|
||||
|
||||
t.equal(doc?.value, 2);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.rejects(
|
||||
() => GetNextCounterValue("fake-counter"),
|
||||
"Could not find sequence document for fake-counter."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
|
||||
@@ -28,6 +28,31 @@ export async function GetNextCounterValue(counterName: string): Promise<integer>
|
||||
return sequenceDoc.value;
|
||||
}
|
||||
|
||||
export async function DecrementCounterValue(counterName: string): Promise<integer> {
|
||||
logger.verbose(`Decrementing Counter Value ${counterName}.`);
|
||||
|
||||
const sequenceDoc = await db.counters.findOneAndUpdate(
|
||||
{
|
||||
counterName,
|
||||
},
|
||||
{
|
||||
$inc: {
|
||||
value: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (!sequenceDoc) {
|
||||
logger.error(`Could not find sequence document for ${counterName}`);
|
||||
throw new Error(`Could not find sequence document for ${counterName}.`);
|
||||
}
|
||||
|
||||
return sequenceDoc.value;
|
||||
}
|
||||
|
||||
export async function GetRelevantSongsAndCharts(
|
||||
scores: (ScoreDocument | PBScoreDocument)[],
|
||||
game: Game
|
||||
|
||||
@@ -69,20 +69,6 @@ export function IsValidPlaytype(game: Game, str: string): str is Playtypes[Game]
|
||||
return GetGameConfig(game).validPlaytypes.includes(str as Playtypes[Game]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a game and playtype depending on how many possible playtypes
|
||||
* this game has.
|
||||
*/
|
||||
export function FormatGPT(game: Game, playtype: Playtypes[Game]) {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
|
||||
if (gameConfig.validPlaytypes.length === 1) {
|
||||
return game;
|
||||
}
|
||||
|
||||
return `${game} (${playtype})`;
|
||||
}
|
||||
|
||||
export function IsString(val: unknown): val is string {
|
||||
return typeof val === "string";
|
||||
}
|
||||
|
||||
@@ -49,6 +49,20 @@ export function GetUserCaseInsensitive(
|
||||
) as Promise<FindOneResult<PublicUserDocument>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user with this email.
|
||||
*/
|
||||
export function GetUserWithEmail(email: string): Promise<FindOneResult<PublicUserDocument>> {
|
||||
return db.users.findOne(
|
||||
{
|
||||
email,
|
||||
},
|
||||
{
|
||||
projection: OMIT_PRIVATE_USER_RETURNS,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns GetUserCaseInsensitive, but without the private field omission.
|
||||
* @see GetUserCaseInsensitive
|
||||
@@ -157,7 +171,7 @@ export async function GetUsersRanking(stats: UserGameStats) {
|
||||
$sum: {
|
||||
$cond: {
|
||||
if: {
|
||||
$gte: [
|
||||
$gt: [
|
||||
`$ratings.${gptConfig.defaultProfileRatingAlg}`,
|
||||
stats.ratings[gptConfig.defaultProfileRatingAlg],
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"typeRoots": [ "@types", "node_modules/@types" ]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.ts", "scripts/single-use/counters.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
Reference in New Issue
Block a user