mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-13 01:38:29 +03:00
Refactor user authentication to not use PrivateUserDocuments
This commit is contained in:
Vendored
+5
-2
@@ -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}`;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user