| import mongoose, { FilterQuery } from 'mongoose'; |
| import type { IUser, BalanceConfig, CreateUserRequest, UserDeleteResult } from '~/types'; |
| import { signPayload } from '~/crypto'; |
|
|
| |
| export function createUserMethods(mongoose: typeof import('mongoose')) { |
| |
| |
| |
| |
| function normalizeEmailInCriteria<T extends FilterQuery<IUser>>(criteria: T): T { |
| const normalized = { ...criteria }; |
| if (typeof normalized.email === 'string') { |
| normalized.email = normalized.email.trim().toLowerCase(); |
| } |
| if (Array.isArray(normalized.$or)) { |
| normalized.$or = normalized.$or.map((condition) => { |
| if (typeof condition.email === 'string') { |
| return { ...condition, email: condition.email.trim().toLowerCase() }; |
| } |
| return condition; |
| }); |
| } |
| return normalized; |
| } |
|
|
| |
| |
| |
| |
| async function findUser( |
| searchCriteria: FilterQuery<IUser>, |
| fieldsToSelect?: string | string[] | null, |
| ): Promise<IUser | null> { |
| const User = mongoose.models.User; |
| const normalizedCriteria = normalizeEmailInCriteria(searchCriteria); |
| const query = User.findOne(normalizedCriteria); |
| if (fieldsToSelect) { |
| query.select(fieldsToSelect); |
| } |
| return (await query.lean()) as IUser | null; |
| } |
|
|
| |
| |
| |
| async function countUsers(filter: FilterQuery<IUser> = {}): Promise<number> { |
| const User = mongoose.models.User; |
| return await User.countDocuments(filter); |
| } |
|
|
| |
| |
| |
| async function createUser( |
| data: CreateUserRequest, |
| balanceConfig?: BalanceConfig, |
| disableTTL: boolean = true, |
| returnUser: boolean = false, |
| ): Promise<mongoose.Types.ObjectId | Partial<IUser>> { |
| const User = mongoose.models.User; |
| const Balance = mongoose.models.Balance; |
|
|
| const userData: Partial<IUser> = { |
| ...data, |
| expiresAt: disableTTL ? undefined : new Date(Date.now() + 604800 * 1000), |
| }; |
|
|
| if (disableTTL) { |
| delete userData.expiresAt; |
| } |
|
|
| const user = await User.create(userData); |
|
|
| |
| if (balanceConfig?.enabled && balanceConfig?.startBalance) { |
| const update: { |
| $inc: { tokenCredits: number }; |
| $set?: { |
| autoRefillEnabled: boolean; |
| refillIntervalValue: number; |
| refillIntervalUnit: string; |
| refillAmount: number; |
| }; |
| } = { |
| $inc: { tokenCredits: balanceConfig.startBalance }, |
| }; |
|
|
| if ( |
| balanceConfig.autoRefillEnabled && |
| balanceConfig.refillIntervalValue != null && |
| balanceConfig.refillIntervalUnit != null && |
| balanceConfig.refillAmount != null |
| ) { |
| update.$set = { |
| autoRefillEnabled: true, |
| refillIntervalValue: balanceConfig.refillIntervalValue, |
| refillIntervalUnit: balanceConfig.refillIntervalUnit, |
| refillAmount: balanceConfig.refillAmount, |
| }; |
| } |
|
|
| await Balance.findOneAndUpdate({ user: user._id }, update, { |
| upsert: true, |
| new: true, |
| }).lean(); |
| } |
|
|
| if (returnUser) { |
| return user.toObject() as Partial<IUser>; |
| } |
| return user._id as mongoose.Types.ObjectId; |
| } |
|
|
| |
| |
| |
| async function updateUser(userId: string, updateData: Partial<IUser>): Promise<IUser | null> { |
| const User = mongoose.models.User; |
| const updateOperation = { |
| $set: updateData, |
| $unset: { expiresAt: '' }, |
| }; |
| return (await User.findByIdAndUpdate(userId, updateOperation, { |
| new: true, |
| runValidators: true, |
| }).lean()) as IUser | null; |
| } |
|
|
| |
| |
| |
| async function getUserById( |
| userId: string, |
| fieldsToSelect?: string | string[] | null, |
| ): Promise<IUser | null> { |
| const User = mongoose.models.User; |
| const query = User.findById(userId); |
| if (fieldsToSelect) { |
| query.select(fieldsToSelect); |
| } |
| return (await query.lean()) as IUser | null; |
| } |
|
|
| |
| |
| |
| async function deleteUserById(userId: string): Promise<UserDeleteResult> { |
| try { |
| const User = mongoose.models.User; |
| const result = await User.deleteOne({ _id: userId }); |
| if (result.deletedCount === 0) { |
| return { deletedCount: 0, message: 'No user found with that ID.' }; |
| } |
| return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' }; |
| } catch (error: unknown) { |
| const errorMessage = error instanceof Error ? error.message : 'Unknown error'; |
| throw new Error('Error deleting user: ' + errorMessage); |
| } |
| } |
|
|
| |
| |
| |
| async function generateToken(user: IUser): Promise<string> { |
| if (!user) { |
| throw new Error('No user provided'); |
| } |
|
|
| let expires = 1000 * 60 * 15; |
|
|
| if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') { |
| try { |
| const evaluated = eval(process.env.SESSION_EXPIRY); |
| if (evaluated) { |
| expires = evaluated; |
| } |
| } catch (error) { |
| console.warn('Invalid SESSION_EXPIRY expression, using default:', error); |
| } |
| } |
|
|
| return await signPayload({ |
| payload: { |
| id: user._id, |
| username: user.username, |
| provider: user.provider, |
| email: user.email, |
| }, |
| secret: process.env.JWT_SECRET, |
| expirationTime: expires / 1000, |
| }); |
| } |
|
|
| |
| |
| |
| |
| async function toggleUserMemories( |
| userId: string, |
| memoriesEnabled: boolean, |
| ): Promise<IUser | null> { |
| const User = mongoose.models.User; |
|
|
| |
| const user = await User.findById(userId); |
| if (!user) { |
| return null; |
| } |
|
|
| |
| const updateOperation = { |
| $set: { |
| 'personalization.memories': memoriesEnabled, |
| }, |
| }; |
|
|
| return (await User.findByIdAndUpdate(userId, updateOperation, { |
| new: true, |
| runValidators: true, |
| }).lean()) as IUser | null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| const searchUsers = async function ({ |
| searchPattern, |
| limit = 20, |
| fieldsToSelect = null, |
| }: { |
| searchPattern: string; |
| limit?: number; |
| fieldsToSelect?: string | string[] | null; |
| }) { |
| if (!searchPattern || searchPattern.trim().length === 0) { |
| return []; |
| } |
|
|
| const regex = new RegExp(searchPattern.trim(), 'i'); |
| const User = mongoose.models.User; |
|
|
| const query = User.find({ |
| $or: [{ email: regex }, { name: regex }, { username: regex }], |
| }).limit(limit * 2); |
|
|
| if (fieldsToSelect) { |
| query.select(fieldsToSelect); |
| } |
|
|
| const users = await query.lean(); |
|
|
| |
| const exactRegex = new RegExp(`^${searchPattern.trim()}$`, 'i'); |
| const startsWithPattern = searchPattern.trim().toLowerCase(); |
|
|
| const scoredUsers = users.map((user) => { |
| const searchableFields = [user.name, user.email, user.username].filter(Boolean); |
| let maxScore = 0; |
|
|
| for (const field of searchableFields) { |
| const fieldLower = field.toLowerCase(); |
| let score = 0; |
|
|
| |
| if (exactRegex.test(field)) { |
| score = 100; |
| } |
| |
| else if (fieldLower.startsWith(startsWithPattern)) { |
| score = 80; |
| } |
| |
| else if (fieldLower.includes(startsWithPattern)) { |
| score = 50; |
| } |
| |
| else { |
| score = 10; |
| } |
|
|
| maxScore = Math.max(maxScore, score); |
| } |
|
|
| return { ...user, _searchScore: maxScore }; |
| }); |
|
|
| |
| return scoredUsers |
| .sort((a, b) => b._searchScore - a._searchScore) |
| .slice(0, limit) |
| .map((user) => { |
| |
| |
| const { _searchScore, ...userWithoutScore } = user; |
| return userWithoutScore; |
| }); |
| }; |
|
|
| return { |
| findUser, |
| countUsers, |
| createUser, |
| updateUser, |
| searchUsers, |
| getUserById, |
| generateToken, |
| deleteUserById, |
| toggleUserMemories, |
| }; |
| } |
|
|
| export type UserMethods = ReturnType<typeof createUserMethods>; |
|
|