mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-28 09:58:11 +03:00
Add PATCH /users/:userID endpoint
This commit is contained in:
+1
-1
@@ -71,7 +71,7 @@
|
||||
"redis": "3.1.2",
|
||||
"rimraf": "3.0.2",
|
||||
"safe-json-stringify": "1.2.0",
|
||||
"tachi-common": "0.1.38",
|
||||
"tachi-common": "0.1.40",
|
||||
"typescript": "4.3.4",
|
||||
"winston": "3.3.3"
|
||||
},
|
||||
|
||||
Generated
+4
-4
@@ -52,7 +52,7 @@ specifiers:
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
supertest: 6.1.3
|
||||
tachi-common: 0.1.38
|
||||
tachi-common: 0.1.40
|
||||
tap: 15.0.9
|
||||
ts-node: 10.0.0
|
||||
tsconfig-paths: 3.10.1
|
||||
@@ -85,7 +85,7 @@ dependencies:
|
||||
redis: 3.1.2
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
tachi-common: 0.1.38_ts-node@10.0.0+typescript@4.3.4
|
||||
tachi-common: 0.1.40_ts-node@10.0.0+typescript@4.3.4
|
||||
typescript: 4.3.4
|
||||
winston: 3.3.3
|
||||
|
||||
@@ -3666,8 +3666,8 @@ packages:
|
||||
strip-ansi: 6.0.0
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.1.38_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {integrity: sha512-i+6A31CrE9S3K5Ht8KN1LdryM4An8tmtLwlZt4bszAVzt8i4wiVo7yxHxDvPWSk7aGFFTyaVpabbycAyYCa+lg==}
|
||||
/tachi-common/0.1.40_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {integrity: sha512-oj52sHcb2x+Jcg/qyKx1ras/b/9AiW5n1yYa+1pw/6SD8ZcHtzXkSFnUhq0oFbcwPvfy+4m7ygczMfhw9BsN2g==}
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
tap: 15.0.9_ts-node@10.0.0+typescript@4.3.4
|
||||
|
||||
@@ -90,6 +90,7 @@ export async function AddNewUser(
|
||||
email: email,
|
||||
clan: null,
|
||||
socialMedia: {},
|
||||
status: null,
|
||||
customBanner: false,
|
||||
customPfp: false,
|
||||
joinDate: Date.now(),
|
||||
|
||||
@@ -11,7 +11,7 @@ const router: Router = Router({ mergeParams: true });
|
||||
/**
|
||||
* Searches a user's personal bests.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/scores
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/pbs
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
@@ -57,6 +57,35 @@ router.get("/", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns all of a users personal bests.
|
||||
*
|
||||
* @warn This endpoint is probably quite expensive. We'll need to do
|
||||
* some performance tests.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/pbs/all
|
||||
*/
|
||||
router.get("/all", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
const pbs = await db["personal-bests"].find({
|
||||
userID: user.id,
|
||||
game,
|
||||
playtype,
|
||||
isPrimary: true,
|
||||
});
|
||||
|
||||
const { songs, charts } = await GetRelevantSongsAndCharts(pbs, game);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Returned ${pbs.length} PBs.`,
|
||||
body: { pbs, songs, charts },
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a users best 100 personal-bests for this game.
|
||||
*
|
||||
|
||||
@@ -44,6 +44,13 @@ export const GetUserFromParam: RequestHandler = async (req, res, next) => {
|
||||
export const RequireAuthedAsUser: RequestHandler = (req, res, next) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
|
||||
if (!req[SYMBOL_TachiAPIAuth].userID) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
description: `Authentication is required for this endpoint.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (req[SYMBOL_TachiAPIAuth].userID !== user.id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
|
||||
@@ -69,6 +69,187 @@ t.test("GET /api/v1/users/:userID", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("PATCH /api/v1/users/:userID", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(async () => {
|
||||
await db["api-tokens"].insert({
|
||||
identifier: "customiseProfile",
|
||||
permissions: {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "valid_token",
|
||||
userID: 1,
|
||||
});
|
||||
});
|
||||
|
||||
t.test("Should require authentication", async (t) => {
|
||||
const res = await mockApi.patch("/api/v1/users/1");
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
await db["api-tokens"].insert({
|
||||
token: "noperm",
|
||||
permissions: {},
|
||||
identifier: "No permissions token",
|
||||
userID: 1,
|
||||
});
|
||||
|
||||
const res2 = await mockApi.patch("/api/v1/users/1").set("Authorization", "Bearer noperm");
|
||||
|
||||
t.equal(res2.statusCode, 403);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject empty updates.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token");
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should update the user doc.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
status: "Hello World!",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.status, "Hello World!");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.status, "Hello World!");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Shouldn't alter other properties.", async (t) => {
|
||||
await db.users.update({ id: 1 }, { $set: { "socialMedia.discord": "foo#123" } });
|
||||
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
status: "Hello World!",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.status, "Hello World!");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.status, "Hello World!");
|
||||
t.equal(dbUser?.about, "test_user_not_real");
|
||||
t.equal(dbUser?.socialMedia.discord, "foo#123");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly strip twitter urls.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
twitter: "https://twitter.com/zkldi",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.socialMedia.twitter, "zkldi");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.socialMedia.twitter, "zkldi");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly strip youtube urls.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
youtube: "https://youtube.com/user/zkldi",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.socialMedia.youtube, "zkldi");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.socialMedia.youtube, "zkldi");
|
||||
|
||||
const res2 = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
youtube: "https://youtube.com/channel/zkldi",
|
||||
});
|
||||
|
||||
t.equal(res2.body.body.socialMedia.youtube, "zkldi");
|
||||
|
||||
const dbUser2 = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser2?.socialMedia.youtube, "zkldi");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly strip github urls.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
github: "https://github.com/zkldi",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.socialMedia.github, "zkldi");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.socialMedia.github, "zkldi");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly strip twitch urls.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
twitch: "https://twitch.tv/zkldi",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.socialMedia.twitch, "zkldi");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.socialMedia.twitch, "zkldi");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should correctly strip steam urls.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1")
|
||||
.set("Authorization", "Bearer valid_token")
|
||||
.send({
|
||||
steam: "https://steamcommunity.com/id/zkldi",
|
||||
});
|
||||
|
||||
t.equal(res.body.body.socialMedia.steam, "zkldi");
|
||||
|
||||
const dbUser = await db.users.findOne({ id: 1 });
|
||||
|
||||
t.equal(dbUser?.socialMedia.steam, "zkldi");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/game-stats", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import { GetUserFromParam } from "./middleware";
|
||||
import { GetUserFromParam, RequireAuthedAsUser } from "./middleware";
|
||||
import gamePTRouter from "./games/_game/_playtype/router";
|
||||
import bannerRouter from "./banner/router";
|
||||
import pfpRouter from "./pfp/router";
|
||||
import integrationsRouter from "./integrations/router";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import p from "prudence";
|
||||
import { optNull, optNullFluffStrField } from "utils/prudence";
|
||||
import { DeleteUndefinedProps, StripUrl } from "utils/misc";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -25,6 +31,115 @@ router.get("/", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
interface UserPatchBody {
|
||||
about?: string | null;
|
||||
status?: string | null;
|
||||
discord?: string | null;
|
||||
twitter?: string | null;
|
||||
twitch?: string | null;
|
||||
youtube?: string | null;
|
||||
github?: string | null;
|
||||
steam?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify this user document. All parameters are optional.
|
||||
*
|
||||
* @param about - An about me, this is rendered as markdown.
|
||||
* @param status - A user status. This is not rendered as markdown, and is short.
|
||||
* @param discord - The user's discord tag.
|
||||
* @param twitter - The user's twitter tag.
|
||||
* @param github - The user's github.
|
||||
* @param steam - The user's steamID.
|
||||
* @param youtube - The user's youtube.
|
||||
* @param twitch - The user's twitch.
|
||||
*
|
||||
* @name PATCH /api/v1/users/:userID
|
||||
*/
|
||||
router.patch(
|
||||
"/",
|
||||
RequireAuthedAsUser,
|
||||
RequirePermissions("customise_profile"),
|
||||
prValidate({
|
||||
about: optNull(p.isBoundedString(3, 2000)),
|
||||
status: optNullFluffStrField,
|
||||
discord: optNullFluffStrField,
|
||||
twitter: optNullFluffStrField,
|
||||
github: optNullFluffStrField,
|
||||
steam: optNullFluffStrField,
|
||||
youtube: optNullFluffStrField,
|
||||
twitch: optNullFluffStrField,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
|
||||
if (Object.keys(req.body).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Nothing was provided to modify.`,
|
||||
});
|
||||
}
|
||||
|
||||
const body: UserPatchBody = req.body;
|
||||
|
||||
// Hack stuff for user experience.
|
||||
// In kt1, users would repeatedly mess up these fields.
|
||||
if (body.twitter) {
|
||||
body.twitter = StripUrl("twitter.com/", body.twitter);
|
||||
}
|
||||
if (body.github) {
|
||||
body.github = StripUrl("github.com/", body.github);
|
||||
}
|
||||
if (body.youtube) {
|
||||
// youtube has two user urls lol
|
||||
body.youtube = StripUrl("youtube.com/user/", body.youtube);
|
||||
body.youtube = StripUrl("youtube.com/channel/", body.youtube);
|
||||
}
|
||||
if (body.twitch) {
|
||||
body.twitch = StripUrl("twitch.tv/", body.twitch);
|
||||
}
|
||||
if (body.steam) {
|
||||
body.steam = StripUrl("steamcommunity.com/id/", body.steam);
|
||||
}
|
||||
|
||||
// :(
|
||||
const modifyObject: any = {
|
||||
about: body.about,
|
||||
status: body.status,
|
||||
};
|
||||
|
||||
for (const socMed of [
|
||||
"twitch",
|
||||
"github",
|
||||
"youtube",
|
||||
"steam",
|
||||
"twitter",
|
||||
"discord",
|
||||
] as const) {
|
||||
modifyObject[`socialMedia.${socMed}`] = body[socMed];
|
||||
}
|
||||
|
||||
DeleteUndefinedProps(modifyObject);
|
||||
|
||||
await db.users.update(
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
{
|
||||
$set: modifyObject,
|
||||
}
|
||||
);
|
||||
|
||||
const newUser = await GetUserWithID(user.id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Successfully updated user.`,
|
||||
body: newUser,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns all of the game-stats this user has.
|
||||
* This endpoint doubles up as a way of checking what games a user has played.
|
||||
|
||||
@@ -75,4 +75,24 @@ export function IsString(val: unknown): val is string {
|
||||
|
||||
export function DedupeArr<T>(arr: T[]): T[] {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
}
|
||||
|
||||
export function StripUrl(url: string, userInput: string | null) {
|
||||
if (!userInput) {
|
||||
return userInput;
|
||||
}
|
||||
|
||||
if (userInput.toLowerCase().includes(url)) {
|
||||
return userInput.split(url)[1];
|
||||
}
|
||||
|
||||
return userInput;
|
||||
}
|
||||
|
||||
export function DeleteUndefinedProps(record: any) {
|
||||
for (const key in record) {
|
||||
if (record[key] === undefined) {
|
||||
delete record[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,5 @@ export function FormatPrError(err: PrudenceError, foreword = "Error") {
|
||||
}
|
||||
|
||||
export const optNull = (v: ValidSchemaValue) => p.optional(p.nullable(v));
|
||||
|
||||
export const optNullFluffStrField = optNull(p.isBoundedString(3, 140));
|
||||
|
||||
Reference in New Issue
Block a user