mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-06 05:26:54 +08:00
feat: maintain hashed blocklist of banned usernames, emails and discordids (fehmer) (#5371)
* feat: maintain blocklist of banned usernames and email (fehmer) * update privacy policy --------- Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
e99e283191
commit
4589bbf679
15 changed files with 1453 additions and 422 deletions
|
@ -4,11 +4,35 @@ import * as Configuration from "../../../src/init/configuration";
|
|||
import { getCurrentTestActivity } from "../../../src/api/controllers/user";
|
||||
import * as UserDal from "../../../src/dal/user";
|
||||
import _ from "lodash";
|
||||
import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier";
|
||||
import * as AuthUtils from "../../../src/utils/auth";
|
||||
import * as BlocklistDal from "../../../src/dal/blocklist";
|
||||
import * as ApeKeys from "../../../src/dal/ape-keys";
|
||||
import * as PresetDal from "../../../src/dal/preset";
|
||||
import * as ConfigDal from "../../../src/dal/config";
|
||||
import * as ResultDal from "../../../src/dal/result";
|
||||
import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards";
|
||||
import GeorgeQueue from "../../../src/queues/george-queue";
|
||||
import * as AdminUuids from "../../../src/dal/admin-uids";
|
||||
import * as DiscordUtils from "../../../src/utils/discord";
|
||||
|
||||
const mockApp = request(app);
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
|
||||
const mockDecodedToken: DecodedIdToken = {
|
||||
uid: "123456789",
|
||||
email: "newuser@mail.com",
|
||||
iat: Date.now(),
|
||||
} as DecodedIdToken;
|
||||
|
||||
describe("user controller test", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(AuthUtils, "verifyIdToken").mockResolvedValue(mockDecodedToken);
|
||||
});
|
||||
describe("user creation flow", () => {
|
||||
beforeEach(async () => {
|
||||
await enableSignup(true);
|
||||
});
|
||||
it("should be able to check name, sign up, and get user data", async () => {
|
||||
await mockApp
|
||||
.get("/users/checkName/NewUser")
|
||||
|
@ -24,42 +48,6 @@ describe("user controller test", () => {
|
|||
captcha: "captcha",
|
||||
};
|
||||
|
||||
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue({
|
||||
//if stuff breaks this might be the reason
|
||||
users: {
|
||||
signUp: true,
|
||||
discordIntegration: {
|
||||
enabled: false,
|
||||
},
|
||||
autoBan: {
|
||||
enabled: false,
|
||||
maxCount: 5,
|
||||
maxHours: 1,
|
||||
},
|
||||
profiles: {
|
||||
enabled: false,
|
||||
},
|
||||
xp: {
|
||||
enabled: false,
|
||||
gainMultiplier: 0,
|
||||
maxDailyBonus: 0,
|
||||
minDailyBonus: 0,
|
||||
streak: {
|
||||
enabled: false,
|
||||
maxStreakDays: 0,
|
||||
maxStreakMultiplier: 0,
|
||||
},
|
||||
},
|
||||
inbox: {
|
||||
enabled: false,
|
||||
maxMail: 0,
|
||||
},
|
||||
premium: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
await mockApp
|
||||
.post("/users/signup")
|
||||
.set("authorization", "Uid 123456789|newuser@mail.com")
|
||||
|
@ -92,20 +80,133 @@ describe("user controller test", () => {
|
|||
Accept: "application/json",
|
||||
})
|
||||
.expect(409);
|
||||
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
describe("user signup", () => {
|
||||
const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains");
|
||||
const firebaseDeleteUserMock = vi.spyOn(AuthUtils, "deleteUser");
|
||||
const usernameAvailableMock = vi.spyOn(UserDal, "isNameAvailable");
|
||||
beforeEach(async () => {
|
||||
await enableSignup(true);
|
||||
usernameAvailableMock.mockResolvedValue(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
[
|
||||
blocklistContainsMock,
|
||||
firebaseDeleteUserMock,
|
||||
usernameAvailableMock,
|
||||
].forEach((it) => it.mockReset());
|
||||
});
|
||||
|
||||
it("should not create user if blocklisted", async () => {
|
||||
//GIVEN
|
||||
blocklistContainsMock.mockResolvedValue(true);
|
||||
firebaseDeleteUserMock.mockResolvedValue();
|
||||
|
||||
const newUser = {
|
||||
name: "NewUser",
|
||||
uid: "123456789",
|
||||
email: "newuser@mail.com",
|
||||
captcha: "captcha",
|
||||
};
|
||||
|
||||
//WHEN
|
||||
const result = await mockApp
|
||||
.post("/users/signup")
|
||||
.set("authorization", "Uid 123456789|newuser@mail.com")
|
||||
.send(newUser)
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(409);
|
||||
|
||||
//THEN
|
||||
expect(result.body.message).toEqual("Username or email blocked");
|
||||
expect(blocklistContainsMock).toHaveBeenCalledWith({
|
||||
name: "NewUser",
|
||||
email: "newuser@mail.com",
|
||||
});
|
||||
|
||||
//user will be created in firebase from the frontend, make sure we remove it
|
||||
expect(firebaseDeleteUserMock).toHaveBeenCalledWith("123456789");
|
||||
});
|
||||
|
||||
it("should not create user domain is blacklisted", async () => {
|
||||
["tidal.lol", "selfbot.cc"].forEach(async (domain) => {
|
||||
//GIVEN
|
||||
firebaseDeleteUserMock.mockResolvedValue();
|
||||
|
||||
const newUser = {
|
||||
name: "NewUser",
|
||||
uid: "123456789",
|
||||
email: `newuser@${domain}`,
|
||||
captcha: "captcha",
|
||||
};
|
||||
|
||||
//WHEN
|
||||
const result = await mockApp
|
||||
.post("/users/signup")
|
||||
.set("authorization", `Uid 123456789|newuser@${domain}`)
|
||||
.send(newUser)
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
//THEN
|
||||
expect(result.body.message).toEqual("Invalid domain");
|
||||
|
||||
//user will be created in firebase from the frontend, make sure we remove it
|
||||
expect(firebaseDeleteUserMock).toHaveBeenCalledWith("123456789");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not create user if username is taken", async () => {
|
||||
//GIVEN
|
||||
usernameAvailableMock.mockResolvedValue(false);
|
||||
firebaseDeleteUserMock.mockResolvedValue();
|
||||
|
||||
const newUser = {
|
||||
name: "NewUser",
|
||||
uid: "123456789",
|
||||
email: "newuser@mail.com",
|
||||
captcha: "captcha",
|
||||
};
|
||||
|
||||
//WHEN
|
||||
const result = await mockApp
|
||||
.post("/users/signup")
|
||||
.set("authorization", "Uid 123456789|newuser@mail.com")
|
||||
.send(newUser)
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(409);
|
||||
|
||||
//THEN
|
||||
expect(result.body.message).toEqual("Username unavailable");
|
||||
expect(usernameAvailableMock).toHaveBeenCalledWith(
|
||||
"NewUser",
|
||||
"123456789"
|
||||
);
|
||||
|
||||
//user will be created in firebase from the frontend, make sure we remove it
|
||||
expect(firebaseDeleteUserMock).toHaveBeenCalledWith("123456789");
|
||||
});
|
||||
});
|
||||
describe("getTestActivity", () => {
|
||||
const getUserMock = vi.spyOn(UserDal, "getUser");
|
||||
afterAll(() => {
|
||||
getUserMock.mockReset();
|
||||
});
|
||||
it("should return 503 for non premium users", async () => {
|
||||
//given
|
||||
vi.spyOn(UserDal, "getUser").mockResolvedValue({
|
||||
getUserMock.mockResolvedValue({
|
||||
testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] },
|
||||
} as unknown as MonkeyTypes.DBUser);
|
||||
|
||||
//when
|
||||
const response = await mockApp
|
||||
await mockApp
|
||||
.get("/users/testActivity")
|
||||
.set("authorization", "Uid 123456789")
|
||||
.send()
|
||||
|
@ -113,7 +214,7 @@ describe("user controller test", () => {
|
|||
});
|
||||
it("should send data for premium users", async () => {
|
||||
//given
|
||||
vi.spyOn(UserDal, "getUser").mockResolvedValue({
|
||||
getUserMock.mockResolvedValue({
|
||||
testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] },
|
||||
} as unknown as MonkeyTypes.DBUser);
|
||||
vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true);
|
||||
|
@ -200,6 +301,305 @@ describe("user controller test", () => {
|
|||
expect(testsByDays[365]).toEqual(2024094); //2024-01
|
||||
});
|
||||
});
|
||||
|
||||
describe("toggle ban", () => {
|
||||
const getUserMock = vi.spyOn(UserDal, "getUser");
|
||||
const setBannedMock = vi.spyOn(UserDal, "setBanned");
|
||||
const georgeUserBannedMock = vi.spyOn(GeorgeQueue, "userBanned");
|
||||
const isAdminMock = vi.spyOn(AdminUuids, "isAdmin");
|
||||
beforeEach(async () => {
|
||||
await enableAdminFeatures(true);
|
||||
|
||||
isAdminMock.mockResolvedValue(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
[getUserMock, setBannedMock, georgeUserBannedMock, isAdminMock].forEach(
|
||||
(it) => it.mockReset()
|
||||
);
|
||||
});
|
||||
|
||||
it("bans user with discord", async () => {
|
||||
//GIVEN
|
||||
const uid = "myUid";
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
discordId: "discordId",
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
getUserMock.mockResolvedValue(user);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/admin/toggleBan")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send({ uid })
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getUserMock).toHaveBeenLastCalledWith(uid, "toggle ban");
|
||||
expect(setBannedMock).toHaveBeenCalledWith(uid, true);
|
||||
expect(georgeUserBannedMock).toHaveBeenCalledWith("discordId", true);
|
||||
});
|
||||
it("bans user without discord", async () => {
|
||||
//GIVEN
|
||||
const uid = "myUid";
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
discordId: "",
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
getUserMock.mockResolvedValue(user);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/admin/toggleBan")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send({ uid })
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(georgeUserBannedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
it("unbans user with discord", async () => {
|
||||
//GIVEN
|
||||
const uid = "myUid";
|
||||
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
discordId: "discordId",
|
||||
banned: true,
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
getUserMock.mockResolvedValue(user);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/admin/toggleBan")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send({ uid })
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(getUserMock).toHaveBeenLastCalledWith(uid, "toggle ban");
|
||||
expect(setBannedMock).toHaveBeenCalledWith(uid, false);
|
||||
expect(georgeUserBannedMock).toHaveBeenCalledWith("discordId", false);
|
||||
});
|
||||
it("unbans user without discord", async () => {
|
||||
//GIVEN
|
||||
const uid = "myUid";
|
||||
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
discordId: "",
|
||||
banned: true,
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
getUserMock.mockResolvedValue(user);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/admin/toggleBan")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send({ uid })
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(georgeUserBannedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete user", () => {
|
||||
const getUserMock = vi.spyOn(UserDal, "getUser");
|
||||
const deleteUserMock = vi.spyOn(UserDal, "deleteUser");
|
||||
const firebaseDeleteUserMock = vi.spyOn(AuthUtils, "deleteUser");
|
||||
const deleteAllApeKeysMock = vi.spyOn(ApeKeys, "deleteAllApeKeys");
|
||||
const deleteAllPresetsMock = vi.spyOn(PresetDal, "deleteAllPresets");
|
||||
const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig");
|
||||
const deleteAllResultMock = vi.spyOn(ResultDal, "deleteAll");
|
||||
const purgeUserFromDailyLeaderboardsMock = vi.spyOn(
|
||||
DailyLeaderboards,
|
||||
"purgeUserFromDailyLeaderboards"
|
||||
);
|
||||
const blocklistAddMock = vi.spyOn(BlocklistDal, "add");
|
||||
|
||||
beforeEach(() => {
|
||||
[
|
||||
firebaseDeleteUserMock,
|
||||
deleteUserMock,
|
||||
blocklistAddMock,
|
||||
deleteAllApeKeysMock,
|
||||
deleteAllPresetsMock,
|
||||
deleteConfigMock,
|
||||
purgeUserFromDailyLeaderboardsMock,
|
||||
].forEach((it) => it.mockResolvedValue(undefined));
|
||||
|
||||
deleteAllResultMock.mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
[
|
||||
getUserMock,
|
||||
deleteUserMock,
|
||||
blocklistAddMock,
|
||||
firebaseDeleteUserMock,
|
||||
deleteConfigMock,
|
||||
deleteAllResultMock,
|
||||
deleteAllApeKeysMock,
|
||||
deleteAllPresetsMock,
|
||||
purgeUserFromDailyLeaderboardsMock,
|
||||
].forEach((it) => it.mockReset());
|
||||
});
|
||||
|
||||
it("should add user to blocklist if banned", async () => {
|
||||
//GIVEN
|
||||
const uid = mockDecodedToken.uid;
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
discordId: "discordId",
|
||||
banned: true,
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
await getUserMock.mockResolvedValue(user);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.delete("/users/")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(blocklistAddMock).toHaveBeenCalledWith(user);
|
||||
|
||||
expect(deleteUserMock).toHaveBeenCalledWith(uid);
|
||||
expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteConfigMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
|
||||
expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
|
||||
uid,
|
||||
(await configuration).dailyLeaderboards
|
||||
);
|
||||
});
|
||||
it("should delete user without adding to blocklist if not banned", async () => {
|
||||
//GIVEN
|
||||
const uid = mockDecodedToken.uid;
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
discordId: "discordId",
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
getUserMock.mockResolvedValue(user);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.delete("/users/")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(blocklistAddMock).not.toHaveBeenCalled();
|
||||
|
||||
expect(deleteUserMock).toHaveBeenCalledWith(uid);
|
||||
expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteConfigMock).toHaveBeenCalledWith(uid);
|
||||
expect(deleteAllResultMock).toHaveBeenCalledWith(uid);
|
||||
expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith(
|
||||
uid,
|
||||
(await configuration).dailyLeaderboards
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("link discord", () => {
|
||||
const getUserMock = vi.spyOn(UserDal, "getUser");
|
||||
const isDiscordIdAvailableMock = vi.spyOn(UserDal, "isDiscordIdAvailable");
|
||||
const isStateValidForUserMock = vi.spyOn(
|
||||
DiscordUtils,
|
||||
"iStateValidForUser"
|
||||
);
|
||||
const getDiscordUserMock = vi.spyOn(DiscordUtils, "getDiscordUser");
|
||||
const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains");
|
||||
|
||||
beforeEach(async () => {
|
||||
isStateValidForUserMock.mockResolvedValue(true);
|
||||
getDiscordUserMock.mockResolvedValue({
|
||||
id: "discordUserId",
|
||||
avatar: "discorUserAvatar",
|
||||
username: "discordUserName",
|
||||
discriminator: "discordUserDiscriminator",
|
||||
});
|
||||
isDiscordIdAvailableMock.mockResolvedValue(true);
|
||||
blocklistContainsMock.mockResolvedValue(false);
|
||||
await enableDiscordIntegration(true);
|
||||
});
|
||||
afterEach(() => {
|
||||
[
|
||||
getUserMock,
|
||||
isStateValidForUserMock,
|
||||
isDiscordIdAvailableMock,
|
||||
getDiscordUserMock,
|
||||
].forEach((it) => it.mockReset());
|
||||
});
|
||||
|
||||
it("should not link if discordId is blocked", async () => {
|
||||
//GIVEN
|
||||
const uid = mockDecodedToken.uid;
|
||||
const user = {
|
||||
uid,
|
||||
name: "name",
|
||||
email: "email",
|
||||
} as unknown as MonkeyTypes.DBUser;
|
||||
getUserMock.mockResolvedValue(user);
|
||||
blocklistContainsMock.mockResolvedValue(true);
|
||||
|
||||
//WHEN
|
||||
const result = await mockApp
|
||||
.post("/users/discord/link")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.set({
|
||||
Accept: "application/json",
|
||||
})
|
||||
.send({
|
||||
tokenType: "tokenType",
|
||||
accessToken: "accessToken",
|
||||
state: "statestatestatestate",
|
||||
})
|
||||
.expect(409);
|
||||
|
||||
//THEN
|
||||
expect(result.body.message).toEqual("The Discord account is blocked");
|
||||
|
||||
expect(blocklistContainsMock).toBeCalledWith({
|
||||
discordId: "discordUserId",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function fillYearWithDay(days: number): number[] {
|
||||
|
@ -210,8 +610,6 @@ function fillYearWithDay(days: number): number[] {
|
|||
return result;
|
||||
}
|
||||
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
|
||||
async function enablePremiumFeatures(premium: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
users: { premium: { enabled: premium } },
|
||||
|
@ -221,3 +619,33 @@ async function enablePremiumFeatures(premium: boolean): Promise<void> {
|
|||
mockConfig
|
||||
);
|
||||
}
|
||||
|
||||
async function enableAdminFeatures(enabled: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
admin: { endpointsEnabled: enabled },
|
||||
});
|
||||
|
||||
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
|
||||
mockConfig
|
||||
);
|
||||
}
|
||||
|
||||
async function enableSignup(enabled: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
users: { signUp: enabled },
|
||||
});
|
||||
|
||||
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
|
||||
mockConfig
|
||||
);
|
||||
}
|
||||
|
||||
async function enableDiscordIntegration(enabled: boolean): Promise<void> {
|
||||
const mockConfig = _.merge(await configuration, {
|
||||
users: { discordIntegration: { enabled } },
|
||||
});
|
||||
|
||||
vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue(
|
||||
mockConfig
|
||||
);
|
||||
}
|
||||
|
|
339
backend/__tests__/dal/blocklist.spec.ts
Normal file
339
backend/__tests__/dal/blocklist.spec.ts
Normal file
|
@ -0,0 +1,339 @@
|
|||
import { ObjectId } from "mongodb";
|
||||
import * as BlacklistDal from "../../src/dal/blocklist";
|
||||
|
||||
describe("BlocklistDal", () => {
|
||||
describe("add", () => {
|
||||
beforeEach(() => {
|
||||
vitest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vitest.useRealTimers();
|
||||
});
|
||||
it("adds user", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vitest.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name, email });
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
BlacklistDal.getCollection().findOne({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
expect(
|
||||
BlacklistDal.getCollection().findOne({
|
||||
usernameHash: BlacklistDal.hash(name),
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
usernameHash: BlacklistDal.hash(name),
|
||||
timestamp: now,
|
||||
});
|
||||
});
|
||||
it("adds user with discordId", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vitest.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
BlacklistDal.getCollection().findOne({
|
||||
discordIdHash: BlacklistDal.hash(discordId),
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
discordIdHash: BlacklistDal.hash(discordId),
|
||||
timestamp: now,
|
||||
});
|
||||
});
|
||||
it("adds user should not create duplicate name", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vitest.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const email2 = `${name}@otherdomain.com`;
|
||||
await BlacklistDal.add({ name, email });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name, email: email2 });
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
usernameHash: BlacklistDal.hash(name),
|
||||
})
|
||||
.toArray()
|
||||
).resolves.toHaveLength(1);
|
||||
expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
})
|
||||
.toArray()
|
||||
).resolves.toHaveLength(1);
|
||||
expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
emailHash: BlacklistDal.hash(email2),
|
||||
})
|
||||
.toArray()
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
it("adds user should not create duplicate email", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vitest.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const name2 = "user" + new ObjectId().toHexString();
|
||||
await BlacklistDal.add({ name, email });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name: name2, email });
|
||||
|
||||
//THEN
|
||||
expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
emailHash: BlacklistDal.hash(email),
|
||||
})
|
||||
.toArray()
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
it("adds user should not create duplicate discordId", async () => {
|
||||
//GIVEN
|
||||
const now = 1715082588;
|
||||
vitest.setSystemTime(now);
|
||||
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const name2 = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.add({ name: name2, email, discordId });
|
||||
|
||||
//THEN
|
||||
|
||||
expect(
|
||||
BlacklistDal.getCollection()
|
||||
.find({
|
||||
discordIdHash: BlacklistDal.hash(discordId),
|
||||
})
|
||||
.toArray()
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
});
|
||||
describe("contains", () => {
|
||||
it("contains user", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN / THEN
|
||||
//by name
|
||||
expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ name: name.toUpperCase() })
|
||||
).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ name, email: "unknown", discordId: "unknown" })
|
||||
).resolves.toBeTruthy();
|
||||
|
||||
//by email
|
||||
expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ email: email.toUpperCase() })
|
||||
).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ name: "unknown", email, discordId: "unknown" })
|
||||
).resolves.toBeTruthy();
|
||||
|
||||
//by discordId
|
||||
expect(BlacklistDal.contains({ discordId })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ discordId: discordId.toUpperCase() })
|
||||
).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ name: "unknown", email: "unknown", discordId })
|
||||
).resolves.toBeTruthy();
|
||||
|
||||
//by name and email and discordId
|
||||
expect(
|
||||
BlacklistDal.contains({ name, email, discordId })
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("does not contain user", async () => {
|
||||
//GIVEN
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
await BlacklistDal.add({ name: "test2", email: "test2@example.com" });
|
||||
|
||||
//WHEN / THEN
|
||||
expect(BlacklistDal.contains({ name: "unknown" })).resolves.toBeFalsy();
|
||||
expect(BlacklistDal.contains({ email: "unknown" })).resolves.toBeFalsy();
|
||||
expect(
|
||||
BlacklistDal.contains({ discordId: "unknown" })
|
||||
).resolves.toBeFalsy();
|
||||
expect(
|
||||
BlacklistDal.contains({
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
discordId: "unknown",
|
||||
})
|
||||
).resolves.toBeFalsy();
|
||||
|
||||
expect(BlacklistDal.contains({})).resolves.toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("removes existing username", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
await BlacklistDal.add({ name, email });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ name });
|
||||
|
||||
//THEN
|
||||
expect(BlacklistDal.contains({ name })).resolves.toBeFalsy();
|
||||
expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
|
||||
//decoy still exists
|
||||
expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" })
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("removes existing email", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
await BlacklistDal.add({ name, email });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ email });
|
||||
|
||||
//THEN
|
||||
expect(BlacklistDal.contains({ email })).resolves.toBeFalsy();
|
||||
expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
|
||||
//decoy still exists
|
||||
expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" })
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("removes existing discordId", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({
|
||||
name: "test",
|
||||
email: "test@example.com",
|
||||
discordId: "testDiscordId",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ discordId });
|
||||
|
||||
//THEN
|
||||
expect(BlacklistDal.contains({ discordId })).resolves.toBeFalsy();
|
||||
expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
|
||||
//decoy still exists
|
||||
expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" })
|
||||
).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ discordId: "testDiscordId" })
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("removes existing username,email and discordId", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({
|
||||
name: "test",
|
||||
email: "test@example.com",
|
||||
discordId: "testDiscordId",
|
||||
});
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({ name, email, discordId });
|
||||
|
||||
//THEN
|
||||
expect(BlacklistDal.contains({ email })).resolves.toBeFalsy();
|
||||
expect(BlacklistDal.contains({ name })).resolves.toBeFalsy();
|
||||
expect(BlacklistDal.contains({ discordId })).resolves.toBeFalsy();
|
||||
|
||||
//decoy still exists
|
||||
expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ email: "test@example.com" })
|
||||
).resolves.toBeTruthy();
|
||||
expect(
|
||||
BlacklistDal.contains({ discordId: "testDiscordId" })
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not remove for empty user", async () => {
|
||||
//GIVEN
|
||||
const name = "user" + new ObjectId().toHexString();
|
||||
const email = `${name}@example.com`;
|
||||
const discordId = `${name}DiscordId`;
|
||||
await BlacklistDal.add({ name, email, discordId });
|
||||
await BlacklistDal.add({ name: "test", email: "test@example.com" });
|
||||
|
||||
//WHEN
|
||||
await BlacklistDal.remove({});
|
||||
|
||||
//THEN
|
||||
expect(BlacklistDal.contains({ email })).resolves.toBeTruthy();
|
||||
expect(BlacklistDal.contains({ name })).resolves.toBeTruthy();
|
||||
expect(BlacklistDal.contains({ discordId })).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe("hash", () => {
|
||||
it("hashes case insensitive", () => {
|
||||
["test", "TEST", "tESt"].forEach((value) =>
|
||||
expect(BlacklistDal.hash(value)).toEqual(
|
||||
"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
14
backend/__tests__/global-setup.ts
Normal file
14
backend/__tests__/global-setup.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as MongoDbMock from "vitest-mongodb";
|
||||
export async function setup({ provide }): Promise<void> {
|
||||
await MongoDbMock.setup({
|
||||
serverOptions: {
|
||||
binary: {
|
||||
version: "6.0.12",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function teardown(): Promise<void> {
|
||||
await MongoDbMock.teardown();
|
||||
}
|
|
@ -1,78 +1,77 @@
|
|||
import { Collection, Db, MongoClient, WithId } from "mongodb";
|
||||
import { afterAll, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
import { afterAll, beforeAll, afterEach } from "vitest";
|
||||
import * as MongoDbMock from "vitest-mongodb";
|
||||
|
||||
process.env["MODE"] = "dev";
|
||||
|
||||
vi.mock("../src/init/db", () => ({
|
||||
__esModule: true,
|
||||
getDb: (): Db => db,
|
||||
collection: <T>(name: string): Collection<WithId<T>> =>
|
||||
db.collection<WithId<T>>(name),
|
||||
close: () => client?.close(),
|
||||
}));
|
||||
|
||||
vi.mock("../src/utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
error: console.error,
|
||||
warning: console.warn,
|
||||
info: console.info,
|
||||
success: console.info,
|
||||
logToDb: console.info,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("swagger-stats", () => ({
|
||||
getMiddleware:
|
||||
() =>
|
||||
(_: unknown, __: unknown, next: () => unknown): void => {
|
||||
next();
|
||||
},
|
||||
}));
|
||||
//process.env["MONGOMS_DISTRO"] = "ubuntu-22.04";
|
||||
|
||||
if (!process.env["REDIS_URI"]) {
|
||||
// use mock if not set
|
||||
process.env["REDIS_URI"] = "redis://mock";
|
||||
}
|
||||
|
||||
// TODO: better approach for this when needed
|
||||
// https://firebase.google.com/docs/rules/unit-tests#run_local_unit_tests_with_the_version_9_javascript_sdk
|
||||
vi.mock("firebase-admin", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
auth: (): unknown => ({
|
||||
verifyIdToken: (
|
||||
_token: string,
|
||||
_checkRevoked: boolean
|
||||
): unknown /* Promise<DecodedIdToken> */ =>
|
||||
Promise.resolve({
|
||||
aud: "mockFirebaseProjectId",
|
||||
auth_time: 123,
|
||||
exp: 1000,
|
||||
uid: "mockUid",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const collectionsForCleanUp = ["users"];
|
||||
|
||||
let db: Db;
|
||||
let client: MongoClient;
|
||||
const collectionsForCleanUp = ["users"];
|
||||
|
||||
beforeAll(async () => {
|
||||
await MongoDbMock.setup({
|
||||
serverOptions: {
|
||||
binary: {
|
||||
version: "6.0.12",
|
||||
},
|
||||
},
|
||||
//don't add any configuration here, add to global-setup.ts instead.
|
||||
});
|
||||
|
||||
client = new MongoClient(globalThis.__MONGO_URI__);
|
||||
await client.connect();
|
||||
db = client.db();
|
||||
|
||||
vi.mock("../src/init/db", () => ({
|
||||
__esModule: true,
|
||||
getDb: (): Db => db,
|
||||
collection: <T>(name: string): Collection<WithId<T>> =>
|
||||
db.collection<WithId<T>>(name),
|
||||
close: () => {},
|
||||
}));
|
||||
|
||||
vi.mock("../src/utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
error: console.error,
|
||||
warning: console.warn,
|
||||
info: console.info,
|
||||
success: console.info,
|
||||
logToDb: console.info,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("swagger-stats", () => ({
|
||||
getMiddleware:
|
||||
() =>
|
||||
(_: unknown, __: unknown, next: () => unknown): void => {
|
||||
next();
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: better approach for this when needed
|
||||
// https://firebase.google.com/docs/rules/unit-tests#run_local_unit_tests_with_the_version_9_javascript_sdk
|
||||
vi.mock("firebase-admin", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
auth: (): unknown => ({
|
||||
verifyIdToken: (
|
||||
_token: string,
|
||||
_checkRevoked: boolean
|
||||
): unknown /* Promise<DecodedIdToken> */ =>
|
||||
Promise.resolve({
|
||||
aud: "mockFirebaseProjectId",
|
||||
auth_time: 123,
|
||||
exp: 1000,
|
||||
uid: "mockUid",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
afterEach(async () => {
|
||||
if (globalThis.__MONGO_URI__) {
|
||||
await Promise.all(
|
||||
collectionsForCleanUp.map((collection) =>
|
||||
|
@ -82,13 +81,12 @@ beforeEach(async () => {
|
|||
}
|
||||
});
|
||||
|
||||
const realDateNow = Date.now;
|
||||
|
||||
afterEach(() => {
|
||||
Date.now = realDateNow;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await client?.close();
|
||||
await MongoDbMock.teardown();
|
||||
// @ts-ignore
|
||||
db = undefined;
|
||||
//@ts-ignore
|
||||
client = undefined;
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
|
|
@ -2,8 +2,13 @@ import _ from "lodash";
|
|||
import * as misc from "../../src/utils/misc";
|
||||
|
||||
describe("Misc Utils", () => {
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("getCurrentDayTimestamp", () => {
|
||||
Date.now = vi.fn(() => 1652743381);
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1652743381);
|
||||
|
||||
const currentDay = misc.getCurrentDayTimestamp();
|
||||
expect(currentDay).toBe(1641600000);
|
||||
|
|
618
backend/package-lock.json
generated
618
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -81,5 +81,8 @@
|
|||
"typescript": "5.3.3",
|
||||
"vitest": "1.6.0",
|
||||
"vitest-mongodb": "0.0.5"
|
||||
},
|
||||
"overrides": {
|
||||
"mongodb-memory-server": "9.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@ import { deleteConfig } from "../../dal/config";
|
|||
import { verify } from "../../utils/captcha";
|
||||
import * as LeaderboardsDAL from "../../dal/leaderboards";
|
||||
import { purgeUserFromDailyLeaderboards } from "../../utils/daily-leaderboards";
|
||||
import { randomBytes } from "crypto";
|
||||
import * as RedisClient from "../../init/redis";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ObjectId } from "mongodb";
|
||||
import * as ReportDAL from "../../dal/report";
|
||||
|
@ -32,6 +30,7 @@ import {
|
|||
} from "../../utils/auth";
|
||||
import * as Dates from "date-fns";
|
||||
import { UTCDateMini } from "@date-fns/utc";
|
||||
import * as BlocklistDal from "../../dal/blocklist";
|
||||
|
||||
async function verifyCaptcha(captcha: string): Promise<void> {
|
||||
if (!(await verify(captcha))) {
|
||||
|
@ -47,28 +46,30 @@ export async function createNewUser(
|
|||
|
||||
try {
|
||||
await verifyCaptcha(captcha);
|
||||
} catch (e) {
|
||||
try {
|
||||
await firebaseDeleteUser(uid);
|
||||
} catch (e) {
|
||||
// user might be deleted on the frontend
|
||||
|
||||
if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) {
|
||||
throw new MonkeyError(400, "Invalid domain");
|
||||
}
|
||||
|
||||
const available = await UserDAL.isNameAvailable(name, uid);
|
||||
if (!available) {
|
||||
throw new MonkeyError(409, "Username unavailable");
|
||||
}
|
||||
|
||||
const blocklisted = await BlocklistDal.contains({ name, email });
|
||||
if (blocklisted) {
|
||||
throw new MonkeyError(409, "Username or email blocked");
|
||||
}
|
||||
|
||||
await UserDAL.addUser(name, email, uid);
|
||||
void Logger.logToDb("user_created", `${name} ${email}`, uid);
|
||||
|
||||
return new MonkeyResponse("User created");
|
||||
} catch (e) {
|
||||
//user was created in firebase from the frontend, remove it
|
||||
await firebaseDeleteUserIgnoreError(uid);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) {
|
||||
throw new MonkeyError(400, "Invalid domain");
|
||||
}
|
||||
|
||||
const available = await UserDAL.isNameAvailable(name, uid);
|
||||
if (!available) {
|
||||
throw new MonkeyError(409, "Username unavailable");
|
||||
}
|
||||
|
||||
await UserDAL.addUser(name, email, uid);
|
||||
void Logger.logToDb("user_created", `${name} ${email}`, uid);
|
||||
|
||||
return new MonkeyResponse("User created");
|
||||
}
|
||||
|
||||
export async function sendVerificationEmail(
|
||||
|
@ -182,10 +183,9 @@ export async function deleteUser(
|
|||
|
||||
const userInfo = await UserDAL.getUser(uid, "delete user");
|
||||
|
||||
// gdpr goes brr, find a different way
|
||||
// if (userInfo.banned) {
|
||||
// throw new MonkeyError(403, "Banned users cannot delete their account");
|
||||
// }
|
||||
if (userInfo.banned === true) {
|
||||
await BlocklistDal.add(userInfo);
|
||||
}
|
||||
|
||||
//cleanup database
|
||||
await Promise.all([
|
||||
|
@ -435,39 +435,24 @@ export async function getUser(
|
|||
export async function getOauthLink(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const connection = RedisClient.getConnection();
|
||||
if (!connection) {
|
||||
throw new MonkeyError(500, "Redis connection not found");
|
||||
}
|
||||
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const token = randomBytes(10).toString("hex");
|
||||
|
||||
//add the token uid pair to reids
|
||||
await connection.setex(`discordoauth:${uid}`, 60, token);
|
||||
|
||||
//build the url
|
||||
const url = DiscordUtils.getOauthLink();
|
||||
const url = await DiscordUtils.getOauthLink(uid);
|
||||
|
||||
//return
|
||||
return new MonkeyResponse("Discord oauth link generated", {
|
||||
url: `${url}&state=${token}`,
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
|
||||
export async function linkDiscord(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const connection = RedisClient.getConnection();
|
||||
if (!connection) {
|
||||
throw new MonkeyError(500, "Redis connection not found");
|
||||
}
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { tokenType, accessToken, state } = req.body;
|
||||
|
||||
const redisToken = await connection.getdel(`discordoauth:${uid}`);
|
||||
|
||||
if (!(redisToken ?? "") || redisToken !== state) {
|
||||
if (!(await DiscordUtils.iStateValidForUser(state, uid))) {
|
||||
throw new MonkeyError(403, "Invalid user token");
|
||||
}
|
||||
|
||||
|
@ -503,6 +488,10 @@ export async function linkDiscord(
|
|||
);
|
||||
}
|
||||
|
||||
if (await BlocklistDal.contains({ discordId })) {
|
||||
throw new MonkeyError(409, "The Discord account is blocked");
|
||||
}
|
||||
|
||||
await UserDAL.linkDiscord(uid, discordId, discordAvatar);
|
||||
|
||||
await GeorgeQueue.linkDiscord(discordId, uid);
|
||||
|
@ -1033,3 +1022,11 @@ export async function getTestActivity(
|
|||
|
||||
return new MonkeyResponse("Test activity data retrieved", user.testActivity);
|
||||
}
|
||||
|
||||
async function firebaseDeleteUserIgnoreError(uid: string): Promise<void> {
|
||||
try {
|
||||
await firebaseDeleteUser(uid);
|
||||
} catch (e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ router.use(
|
|||
invalidMessage: "Admin endpoints are currently disabled.",
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
adminLimit,
|
||||
|
|
116
backend/src/dal/blocklist.ts
Normal file
116
backend/src/dal/blocklist.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
import { Collection } from "mongodb";
|
||||
import * as db from "../init/db";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
type BlocklistEntryProperties = Pick<
|
||||
SharedTypes.User,
|
||||
"name" | "email" | "discordId"
|
||||
>;
|
||||
// Export for use in tests
|
||||
export const getCollection = (): Collection<MonkeyTypes.DBBlocklistEntry> =>
|
||||
db.collection("blocklist");
|
||||
|
||||
export async function add(user: BlocklistEntryProperties): Promise<void> {
|
||||
const timestamp = Date.now();
|
||||
const inserts: Promise<unknown>[] = [];
|
||||
|
||||
const usernameHash = hash(user.name);
|
||||
const emailHash = hash(user.email);
|
||||
inserts.push(
|
||||
getCollection().replaceOne(
|
||||
{ usernameHash },
|
||||
{
|
||||
usernameHash,
|
||||
timestamp,
|
||||
},
|
||||
{ upsert: true }
|
||||
),
|
||||
getCollection().replaceOne(
|
||||
{ emailHash },
|
||||
{
|
||||
emailHash,
|
||||
timestamp,
|
||||
},
|
||||
{ upsert: true }
|
||||
)
|
||||
);
|
||||
|
||||
if (user.discordId !== undefined && user.discordId !== "") {
|
||||
const discordIdHash = hash(user.discordId);
|
||||
inserts.push(
|
||||
getCollection().replaceOne(
|
||||
{ discordIdHash },
|
||||
{
|
||||
discordIdHash,
|
||||
timestamp,
|
||||
},
|
||||
{ upsert: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
await Promise.all(inserts);
|
||||
}
|
||||
|
||||
export async function remove(
|
||||
user: Partial<BlocklistEntryProperties>
|
||||
): Promise<void> {
|
||||
const filter = getFilter(user);
|
||||
if (filter.length === 0) return;
|
||||
await getCollection().deleteMany({ $or: filter });
|
||||
}
|
||||
|
||||
export async function contains(
|
||||
user: Partial<BlocklistEntryProperties>
|
||||
): Promise<boolean> {
|
||||
const filter = getFilter(user);
|
||||
if (filter.length === 0) return false;
|
||||
|
||||
return (
|
||||
(await getCollection().countDocuments({
|
||||
$or: filter,
|
||||
})) !== 0
|
||||
);
|
||||
}
|
||||
export function hash(value: string): string {
|
||||
return createHash("sha256").update(value.toLocaleLowerCase()).digest("hex");
|
||||
}
|
||||
|
||||
function getFilter(
|
||||
user: Partial<BlocklistEntryProperties>
|
||||
): Partial<MonkeyTypes.DBBlocklistEntry>[] {
|
||||
const filter: Partial<MonkeyTypes.DBBlocklistEntry>[] = [];
|
||||
if (user.email !== undefined) {
|
||||
filter.push({ emailHash: hash(user.email) });
|
||||
}
|
||||
if (user.name !== undefined) {
|
||||
filter.push({ usernameHash: hash(user.name) });
|
||||
}
|
||||
if (user.discordId !== undefined) {
|
||||
filter.push({ discordIdHash: hash(user.discordId) });
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
export async function createIndicies(): Promise<void> {
|
||||
await getCollection().createIndex(
|
||||
{ usernameHash: 1 },
|
||||
{
|
||||
unique: true,
|
||||
partialFilterExpression: { usernameHash: { $exists: true } },
|
||||
}
|
||||
);
|
||||
await getCollection().createIndex(
|
||||
{ emailHash: 1 },
|
||||
{
|
||||
unique: true,
|
||||
partialFilterExpression: { emailHash: { $exists: true } },
|
||||
}
|
||||
);
|
||||
await getCollection().createIndex(
|
||||
{ discordIdHash: 1 },
|
||||
{
|
||||
unique: true,
|
||||
partialFilterExpression: { discordIdHash: { $exists: true } },
|
||||
}
|
||||
);
|
||||
}
|
|
@ -14,6 +14,7 @@ import * as EmailClient from "./init/email-client";
|
|||
import { init as initFirebaseAdmin } from "./init/firebase-admin";
|
||||
|
||||
import { createIndicies as leaderboardDbSetup } from "./dal/leaderboards";
|
||||
import { createIndicies as blocklistDbSetup } from "./dal/blocklist";
|
||||
|
||||
async function bootServer(port: number): Promise<Server> {
|
||||
try {
|
||||
|
@ -68,6 +69,9 @@ async function bootServer(port: number): Promise<Server> {
|
|||
Logger.info("Setting up leaderboard indicies...");
|
||||
await leaderboardDbSetup();
|
||||
|
||||
Logger.info("Setting up blocklist indicies...");
|
||||
await blocklistDbSetup();
|
||||
|
||||
recordServerVersion(version);
|
||||
} catch (error) {
|
||||
Logger.error("Failed to boot server");
|
||||
|
|
10
backend/src/types/types.d.ts
vendored
10
backend/src/types/types.d.ts
vendored
|
@ -110,4 +110,14 @@ declare namespace MonkeyTypes {
|
|||
type DBResult = MonkeyTypes.WithObjectId<
|
||||
SharedTypes.DBResult<SharedTypes.Config.Mode>
|
||||
>;
|
||||
|
||||
type BlocklistEntry = {
|
||||
_id: string;
|
||||
usernameHash?: string;
|
||||
emailHash?: string;
|
||||
discordIdHash?: string;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type DBBlocklistEntry = WithObjectId<MonkeyTypes.BlocklistEntry>;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import fetch from "node-fetch";
|
||||
import { isDevEnvironment } from "./misc";
|
||||
import * as RedisClient from "../init/redis";
|
||||
import { randomBytes } from "crypto";
|
||||
import MonkeyError from "./error";
|
||||
|
||||
const BASE_URL = "https://discord.com/api";
|
||||
|
||||
|
@ -34,10 +37,32 @@ export async function getDiscordUser(
|
|||
return (await response.json()) as DiscordUser;
|
||||
}
|
||||
|
||||
export function getOauthLink(): string {
|
||||
export async function getOauthLink(uid: string): Promise<string> {
|
||||
const connection = RedisClient.getConnection();
|
||||
if (!connection) {
|
||||
throw new MonkeyError(500, "Redis connection not found");
|
||||
}
|
||||
const token = randomBytes(10).toString("hex");
|
||||
|
||||
//add the token uid pair to reids
|
||||
await connection.setex(`discordoauth:${uid}`, 60, token);
|
||||
|
||||
return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${
|
||||
isDevEnvironment()
|
||||
? `http%3A%2F%2Flocalhost%3A3000%2Fverify`
|
||||
: `https%3A%2F%2Fmonkeytype.com%2Fverify`
|
||||
}&response_type=token&scope=identify`;
|
||||
}&response_type=token&scope=identify&state=${token}`;
|
||||
}
|
||||
|
||||
export async function iStateValidForUser(
|
||||
state: string,
|
||||
uid: string
|
||||
): Promise<boolean> {
|
||||
const connection = RedisClient.getConnection();
|
||||
if (!connection) {
|
||||
throw new MonkeyError(500, "Redis connection not found");
|
||||
}
|
||||
const redisToken = await connection.getdel(`discordoauth:${uid}`);
|
||||
|
||||
return redisToken === state;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ export default defineConfig({
|
|||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
globalSetup: "__tests__/global-setup.ts",
|
||||
setupFiles: ["__tests__/setup-tests.ts"],
|
||||
pool: "forks",
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
</p>
|
||||
|
||||
<p>Effective date: September 8, 2021</p>
|
||||
<p>Last updated: Oct 24, 2023</p>
|
||||
<p>Last updated: May 20, 2024</p>
|
||||
<p>
|
||||
Thanks for trusting Monkeytype ('Monkeytype', 'we', 'us', 'our') with
|
||||
your personal information! We take our responsibility to you very
|
||||
|
@ -177,6 +177,7 @@
|
|||
<ul>
|
||||
<li>Email</li>
|
||||
<li>Username</li>
|
||||
<li>Discord id and discord avatar id (if you provide it)</li>
|
||||
<li>Information about each typing test</li>
|
||||
<li>Your currently active settings</li>
|
||||
<li>How many typing tests you've started and completed</li>
|
||||
|
@ -221,6 +222,12 @@
|
|||
<li>Display leaderboards</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
If you are found to be cheating or exploiting the website, we may
|
||||
store hashed versions of your username, email and/or discord id to
|
||||
prevent you from creating new accounts.
|
||||
</p>
|
||||
|
||||
<h1 id="Data_Storage">How do we store your data?</h1>
|
||||
<p>Monkeytype securely stores your data using MongoDB.</p>
|
||||
|
||||
|
@ -245,7 +252,10 @@
|
|||
</li>
|
||||
<li>
|
||||
The right to erasure – You have the right to request that Monkeytype
|
||||
erase your personal data, under certain conditions.
|
||||
erase your personal data, under certain conditions. (Hashed data
|
||||
mentioned in the "How will we use your data?" section will not be
|
||||
deleted, as it is essential in preventing the exploitation of the
|
||||
website)
|
||||
</li>
|
||||
<li>
|
||||
The right to restrict processing – You have the right to request
|
||||
|
|
Loading…
Add table
Reference in a new issue