chat / src /routes /login /callback /userSession.ts
Andrew
feat(auth): Persist password and recovery key hashes in user session
6264428
import { getCoupledCookieHash, refreshSessionCookie } from "$lib/server/auth";
import { collections } from "$lib/server/database";
import { ObjectId } from "mongodb";
import { DEFAULT_SETTINGS } from "$lib/types/Settings";
import { error, type Cookies } from "@sveltejs/kit";
import crypto from "crypto";
import { sha256 } from "$lib/utils/sha256";
import { addWeeks } from "date-fns";
import { logger } from "$lib/server/logger";
import type { AuthProvider } from "$lib/types/User";
import { encryptToken } from "$lib/server/tokenEncryption";
export interface UserUpdateData {
authProvider: AuthProvider;
authId: string;
username?: string;
name: string;
email?: string;
avatarUrl?: string;
isAdmin?: boolean;
isEarlyAccess?: boolean;
passwordHash?: string;
recoveryKeyHash?: string;
}
export async function updateUserSession(params: {
userData: UserUpdateData;
locals: App.Locals;
cookies: Cookies;
userAgent?: string;
ip?: string;
hfAccessToken?: string;
}) {
const { userData, locals, cookies, userAgent, ip, hfAccessToken } = params;
logger.info(
{
authProvider: userData.authProvider,
authId: userData.authId,
login_username: userData.username,
login_name: userData.name,
login_email: userData.email,
},
"user login"
);
logger.debug(
{
isAdmin: userData.isAdmin,
isEarlyAccess: userData.isEarlyAccess,
authId: userData.authId,
},
`Updating user ${userData.authId}`
);
// check if user already exists (by authProvider + authId, or legacy hfUserId for HF)
const existingUser =
userData.authProvider === "huggingface"
? await collections.users.findOne({
$or: [
{ authProvider: "huggingface", authId: userData.authId },
{ hfUserId: userData.authId }, // Legacy lookup
],
})
: await collections.users.findOne({
authProvider: userData.authProvider,
authId: userData.authId,
});
let userId = existingUser?._id;
// update session cookie on login
const previousSessionId = locals.sessionId;
const secretSessionId = crypto.randomUUID();
const sessionId = await sha256(secretSessionId);
if (await collections.sessions.findOne({ sessionId })) {
error(500, "Session ID collision");
}
locals.sessionId = sessionId;
// Get cookie hash if coupling is enabled
const coupledCookieHash = await getCoupledCookieHash({ type: "svelte", value: cookies });
const userUpdateData = {
username: userData.username,
name: userData.name,
avatarUrl: userData.avatarUrl,
isAdmin: userData.isAdmin,
isEarlyAccess: userData.isEarlyAccess,
...(userData.authProvider === "huggingface" ? { hfUserId: userData.authId } : {}),
...(userData.passwordHash ? { passwordHash: userData.passwordHash } : {}),
...(userData.recoveryKeyHash ? { recoveryKeyHash: userData.recoveryKeyHash } : {}),
};
if (existingUser) {
// Update existing user
await collections.users.updateOne(
{ _id: existingUser._id },
{
$set: {
...userUpdateData,
authProvider: userData.authProvider,
authId: userData.authId,
email: userData.email,
updatedAt: new Date(),
},
}
);
// remove previous session if it exists and add new one
await collections.sessions.deleteOne({ sessionId: previousSessionId });
await collections.sessions.insertOne({
_id: new ObjectId(),
sessionId: locals.sessionId,
userId: existingUser._id,
createdAt: new Date(),
updatedAt: new Date(),
userAgent,
ip,
expiresAt: addWeeks(new Date(), 2),
...(coupledCookieHash ? { coupledCookieHash } : {}),
});
} else {
// user doesn't exist yet, create a new one
const { insertedId } = await collections.users.insertOne({
_id: new ObjectId(),
createdAt: new Date(),
updatedAt: new Date(),
username: userUpdateData.username,
name: userUpdateData.name,
avatarUrl: userUpdateData.avatarUrl,
isAdmin: userUpdateData.isAdmin,
isEarlyAccess: userUpdateData.isEarlyAccess,
authProvider: userData.authProvider,
authId: userData.authId,
email: userData.email,
...(userData.authProvider === "huggingface" ? { hfUserId: userData.authId } : {}),
...(userData.passwordHash ? { passwordHash: userData.passwordHash } : {}),
...(userData.recoveryKeyHash ? { recoveryKeyHash: userData.recoveryKeyHash } : {}),
});
userId = insertedId;
await collections.sessions.insertOne({
_id: new ObjectId(),
sessionId: locals.sessionId,
userId,
createdAt: new Date(),
updatedAt: new Date(),
userAgent,
ip,
expiresAt: addWeeks(new Date(), 2),
...(coupledCookieHash ? { coupledCookieHash } : {}),
});
// move pre-existing settings to new user
const { matchedCount } = await collections.settings.updateOne(
{ sessionId: previousSessionId },
{
$set: { userId, updatedAt: new Date() },
$unset: { sessionId: "" },
}
);
if (!matchedCount) {
// if no settings found for user, create default settings
await collections.settings.insertOne({
userId,
updatedAt: new Date(),
createdAt: new Date(),
...DEFAULT_SETTINGS,
});
}
}
// refresh session cookie
refreshSessionCookie(cookies, secretSessionId);
// migrate pre-existing conversations
await collections.conversations.updateMany(
{ sessionId: previousSessionId },
{
$set: { userId },
$unset: { sessionId: "" },
}
);
// Manage stored Hugging Face tokens based on auth provider
if (userData.authProvider === "huggingface" && hfAccessToken) {
const encryptedToken = encryptToken(hfAccessToken);
await collections.userTokens.updateOne(
{ userId, provider: "huggingface" },
{
$set: {
encryptedToken,
updatedAt: new Date(),
},
$setOnInsert: {
_id: new ObjectId(),
userId,
provider: "huggingface",
createdAt: new Date(),
},
},
{ upsert: true }
);
} else {
await collections.userTokens.deleteOne({ userId, provider: "huggingface" });
}
return { userId };
}