Refactor user authentication to not use PrivateUserDocuments

This commit is contained in:
zkldi
2021-09-03 01:14:56 +01:00
parent cce36fa1da
commit e14527e367
6 changed files with 65 additions and 115 deletions
+5 -2
View File
@@ -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<UserGoalDocument>("user-goals"),
milestones: monkDB.get<MilestoneDocument>("milestones"),
"user-milestones": monkDB.get<UserMilestoneDocument>("user-milestones"),
users: monkDB.get<PrivateUserDocument>("users"),
users: monkDB.get<PublicUserDocument>("users"),
imports: monkDB.get<ImportDocument>("imports"),
"import-timings": monkDB.get<ImportTimingsDocument>("import-timings"),
sessions: monkDB.get<SessionDocument>("sessions"),
@@ -167,6 +168,7 @@ const db = {
"session-view-cache": monkDB.get<SessionViewDocument>("session-view-cache"),
"arc-saved-profiles": monkDB.get<ARCSavedProfileDocument>("arc-saved-profiles"),
"user-settings": monkDB.get<UserSettings>("user-settings"),
"user-private-information": monkDB.get<PrivateUserInfoDocument>("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}`;
+3 -4
View File
@@ -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<PrivateUserDocument> = {
const matchQuery: FilterQuery<PublicUserDocument> = {
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,
});
}
+16 -6
View File
@@ -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 = {
+22 -6
View File
@@ -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.`,
+2 -34
View File
@@ -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),
+17 -63
View File
@@ -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<string> {
export function GetUserCaseInsensitive(
username: string
): Promise<FindOneResult<PublicUserDocument>> {
return db.users.findOne(
{
usernameLowercase: username.toLowerCase(),
},
{
projection: OMIT_PRIVATE_USER_RETURNS,
}
) 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
*/
export function PRIVATEINFO_GetUserCaseInsensitive(username: string) {
return db.users.findOne({
usernameLowercase: username.toLowerCase(),
});
}) as Promise<FindOneResult<PublicUserDocument>>;
}
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<FindOneResult<PublicUserDocument>> {
return db.users.findOne(
{
id: userID,
},
{
projection: OMIT_PRIVATE_USER_RETURNS,
}
) as Promise<FindOneResult<PublicUserDocument>>;
return db.users.findOne({
id: userID,
}) as Promise<FindOneResult<PublicUserDocument>>;
}
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<PublicUs
return userDoc;
}
/**
* GetUserWithID, but return personal information, too.
* @see GetUserWithID
*/
export function PRIVATEINFO_GetUserWithID(userID: integer) {
return db.users.findOne({
id: userID,
});
}
/**
* Gets a user based on either their username case-insensitively, or a direct lookup of their ID.
* This is used in URLs to resolve the passed user.