diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 2132c7897..261475606 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -11,7 +11,6 @@ import { MilestoneDocument, FolderChartLookup, ImportTimingsDocument, - PrivateUserDocument, ScoreDocument, KaiAuthDocument, UserGameStatsSnapshot, @@ -30,6 +29,8 @@ import { ARCSavedProfileDocument, UserSettings, Game, + PrivateUserInfoDocument, + PublicUserDocument, } from "tachi-common"; import monk, { TMiddleware } from "monk"; import CreateLogCtx from "lib/logger/logger"; @@ -148,7 +149,7 @@ const db = { "user-goals": monkDB.get("user-goals"), milestones: monkDB.get("milestones"), "user-milestones": monkDB.get("user-milestones"), - users: monkDB.get("users"), + users: monkDB.get("users"), imports: monkDB.get("imports"), "import-timings": monkDB.get("import-timings"), sessions: monkDB.get("sessions"), @@ -167,6 +168,7 @@ const db = { "session-view-cache": monkDB.get("session-view-cache"), "arc-saved-profiles": monkDB.get("arc-saved-profiles"), "user-settings": monkDB.get("user-settings"), + "user-private-information": monkDB.get("user-private-information"), }; export type StaticDatabases = @@ -194,6 +196,7 @@ export type StaticDatabases = | "tables" | "game-stats-snapshots" | "arc-saved-profiles" + | "user-private-information" | "user-settings"; export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; diff --git a/server/src/lib/search/search.ts b/server/src/lib/search/search.ts index 900c17dd8..d973e9d27 100644 --- a/server/src/lib/search/search.ts +++ b/server/src/lib/search/search.ts @@ -9,11 +9,11 @@ import { Playtypes, SongDocument, ChartDocument, - PrivateUserDocument, integer, + PublicUserDocument, } from "tachi-common"; import { EscapeStringRegexp } from "utils/misc"; -import { GetOnlineCutoff, OMIT_PRIVATE_USER_RETURNS } from "utils/user"; +import { GetOnlineCutoff } from "utils/user"; import { ServerTypeInfo } from "lib/setup/config"; const logger = CreateLogCtx(__filename); @@ -129,7 +129,7 @@ export function SearchSessions( export function SearchUsersRegExp(search: string, matchOnline = false) { const regexEsc = EscapeStringRegexp(search.toLowerCase()); - const matchQuery: FilterQuery = { + const matchQuery: FilterQuery = { usernameLowercase: { $regex: new RegExp(regexEsc, "u") }, }; @@ -139,7 +139,6 @@ export function SearchUsersRegExp(search: string, matchOnline = false) { return db.users.find(matchQuery, { limit: 25, - projection: OMIT_PRIVATE_USER_RETURNS, }); } diff --git a/server/src/server/router/api/v1/auth/auth.ts b/server/src/server/router/api/v1/auth/auth.ts index 90f50344a..93ddf1fa6 100644 --- a/server/src/server/router/api/v1/auth/auth.ts +++ b/server/src/server/router/api/v1/auth/auth.ts @@ -1,5 +1,5 @@ import bcrypt from "bcryptjs"; -import { integer, PrivateUserDocument, PublicUserDocument, UserSettings } from "tachi-common"; +import { integer, PrivateUserInfoDocument, PublicUserDocument, UserSettings } from "tachi-common"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { FormatUserDoc } from "utils/user"; @@ -85,20 +85,18 @@ export async function AddNewUser( logger.verbose(`Hashed password for ${username}.`); - const userDoc: PrivateUserDocument = { + const userDoc: PublicUserDocument = { id: userID, - username: username, + username, usernameLowercase: username.toLowerCase(), - password: hashedPassword, about: "I'm a fairly nondescript person.", - email: email, clan: null, socialMedia: {}, status: null, customBanner: false, customPfp: false, joinDate: Date.now(), - lastSeen: Date.now(), // lol + lastSeen: Date.now(), authLevel: "user", badges: [], }; @@ -107,9 +105,21 @@ export async function AddNewUser( const settingsRes = await InsertDefaultUserSettings(userID); + await InsertPrivateUserInfo(userID, hashedPassword, email); + return { newUser: res, newSettings: settingsRes }; } +export function InsertPrivateUserInfo(userID: integer, hashedPassword: string, email: string) { + const privateInfo: PrivateUserInfoDocument = { + userID, + email, + password: hashedPassword, + }; + + return db["user-private-information"].insert(privateInfo); +} + export function InsertDefaultUserSettings(userID: integer) { logger.verbose(`Inserting default settings for ${userID}.`); const userSettings: UserSettings = { diff --git a/server/src/server/router/api/v1/auth/router.ts b/server/src/server/router/api/v1/auth/router.ts index 2e430ba66..4d540eb79 100644 --- a/server/src/server/router/api/v1/auth/router.ts +++ b/server/src/server/router/api/v1/auth/router.ts @@ -10,12 +10,12 @@ import { InsertDefaultUserSettings, } from "./auth"; import { + CheckIfEmailInUse, FormatUserDoc, GetSettingsForUser, GetUserCaseInsensitive, - GetUserWithEmail, + GetUserPrivateInfo, GetUserWithID, - PRIVATEINFO_GetUserCaseInsensitive, } from "utils/user"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; @@ -78,7 +78,7 @@ router.post( logger.verbose("Skipped captcha check because not in production."); } - const requestedUser = await PRIVATEINFO_GetUserCaseInsensitive(req.body.username); + const requestedUser = await GetUserCaseInsensitive(req.body.username); if (!requestedUser) { logger.verbose(`Invalid username for login ${req.body.username}.`); @@ -88,7 +88,23 @@ router.post( }); } - const passwordMatch = await PasswordCompare(req.body.password, requestedUser.password); + const privateInfo = await GetUserPrivateInfo(requestedUser.id); + + if (!privateInfo) { + logger.severe( + `State desync for user ${FormatUserDoc( + requestedUser + )}. This user has no password/email information?`, + { requestedUser } + ); + + return res.status(500).json({ + success: false, + description: `An internal server error has occured.`, + }); + } + + const passwordMatch = await PasswordCompare(req.body.password, privateInfo.password); if (!passwordMatch) { logger.verbose("Invalid password provided."); @@ -185,10 +201,10 @@ router.post( }); } - const existingEmail = await GetUserWithEmail(req.body.email); + const existingEmail = await CheckIfEmailInUse(req.body.email); if (existingEmail) { - logger.info(`User attempted to use email ${req.body.email}, but was already in use.`); + logger.info(`User attempted to sign up with email that was already in use.`); return res.status(409).json({ success: false, description: `This email is already in use.`, diff --git a/server/src/utils/user.test.ts b/server/src/utils/user.test.ts index 52938b431..89d4646b5 100644 --- a/server/src/utils/user.test.ts +++ b/server/src/utils/user.test.ts @@ -1,7 +1,7 @@ import t from "tap"; -import { PRUDENCE_PRIVATE_USER, PRUDENCE_PUBLIC_USER } from "external/mongo/schemas"; +import { PRUDENCE_PUBLIC_USER } from "external/mongo/schemas"; import { prAssert } from "test-utils/asserts"; -import { GetUserCaseInsensitive, PRIVATEINFO_GetUserCaseInsensitive, FormatUserDoc } from "./user"; +import { GetUserCaseInsensitive, FormatUserDoc } from "./user"; import { PublicUserDocument } from "tachi-common"; import ResetDBState from "test-utils/resets"; @@ -48,38 +48,6 @@ t.test("#GetUserCaseInsensitive", (t) => { t.end(); }); -t.test("#PRIVATEINFO_GetUserCaseInsensitive", (t) => { - t.beforeEach(ResetDBState); - - t.test("Should return the user for an exact username", async (t) => { - const result = await PRIVATEINFO_GetUserCaseInsensitive("test_zkldi"); - - t.not(result, null, "Should not return null"); - - t.equal(result!.username, "test_zkldi", "Should return test_rzkldi"); - - prAssert(result, PRUDENCE_PRIVATE_USER, "Should return a conforming PrivateUserDocument"); - }); - - t.test("Should return the user for an incorrectly cased username", async (t) => { - const result = await PRIVATEINFO_GetUserCaseInsensitive("tesT_ZkLdi"); - - t.not(result, null, "Should not return null"); - - t.equal(result!.username, "test_zkldi", "Should return test_zkldi"); - - prAssert(result, PRUDENCE_PRIVATE_USER, "Should return a conforming PrivateUserDocument"); - }); - - t.test("Should not return the user for a username that does not exist", async (t) => { - const result = await PRIVATEINFO_GetUserCaseInsensitive("foobar"); - - t.equal(result, null, "Should return null"); - }); - - t.end(); -}); - t.test("#FormatUserDoc", (t) => { t.equal( FormatUserDoc({ username: "zkldi", id: 123 } as PublicUserDocument), diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts index 1829ff5e6..7aa5057e8 100644 --- a/server/src/utils/user.ts +++ b/server/src/utils/user.ts @@ -14,13 +14,6 @@ import CreateLogCtx from "lib/logger/logger"; const logger = CreateLogCtx(__filename); -export const OMIT_PRIVATE_USER_RETURNS = { - password: 0, - email: 0, - // legacy protection - this field does not exist on user documents anymore, but it did, and it *did* hold personal information. - integrations: 0, -}; - /** * Returns a user's username from their ID. Throws if no user with that ID exists. */ @@ -49,52 +42,28 @@ export async function GetUsernameFromUserID(userID: integer): Promise { export function GetUserCaseInsensitive( username: string ): Promise> { - return db.users.findOne( - { - usernameLowercase: username.toLowerCase(), - }, - { - projection: OMIT_PRIVATE_USER_RETURNS, - } - ) as Promise>; -} - -/** - * Returns the user with this email. - */ -export function GetUserWithEmail(email: string): Promise> { - return db.users.findOne( - { - email, - }, - { - projection: OMIT_PRIVATE_USER_RETURNS, - } - ); -} - -/** - * Returns GetUserCaseInsensitive, but without the private field omission. - * @see GetUserCaseInsensitive - */ -export function PRIVATEINFO_GetUserCaseInsensitive(username: string) { return db.users.findOne({ usernameLowercase: username.toLowerCase(), - }); + }) as Promise>; +} + +export async function CheckIfEmailInUse(email: string) { + const doc = await db["user-private-information"].findOne({ email }); + + return !!doc; +} + +export function GetUserPrivateInfo(userID: integer) { + return db["user-private-information"].findOne({ userID }); } /** * Gets a user from their userID. */ export function GetUserWithID(userID: integer): Promise> { - return db.users.findOne( - { - id: userID, - }, - { - projection: OMIT_PRIVATE_USER_RETURNS, - } - ) as Promise>; + return db.users.findOne({ + id: userID, + }) as Promise>; } export function GetSettingsForUser(userID: integer) { @@ -107,14 +76,9 @@ export function GetSettingsForUser(userID: integer) { * Gets the users for these user IDs. */ export function GetUsersWithIDs(userIDs: integer[]) { - return db.users.find( - { - id: { $in: userIDs }, - }, - { - projection: OMIT_PRIVATE_USER_RETURNS, - } - ); + return db.users.find({ + id: { $in: userIDs }, + }); } /** @@ -137,16 +101,6 @@ export async function GetUserWithIDGuaranteed(userID: integer): Promise