mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 02:40:14 +01:00
refactor(data-schemas): restructure schemas, models, and methods for improved modularity
This commit is contained in:
parent
30b8a1c6c4
commit
f6ca8caf7e
17 changed files with 1080 additions and 782 deletions
|
|
@ -1,12 +1,27 @@
|
|||
const bcrypt = require('bcryptjs');
|
||||
const { webcrypto } = require('node:crypto');
|
||||
const {
|
||||
findUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
findToken,
|
||||
countUsers,
|
||||
getUserById,
|
||||
findSession,
|
||||
createToken,
|
||||
deleteTokens,
|
||||
deleteSession,
|
||||
createSession,
|
||||
generateToken,
|
||||
deleteUserById,
|
||||
generateRefreshToken,
|
||||
} = require('@librechat/data-schemas');
|
||||
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
||||
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||
const { getBalanceConfig } = require('~/server/services/Config');
|
||||
const { registerSchema } = require('~/strategies/validators');
|
||||
const { logger } = require('~/config');
|
||||
const db = require('~/lib/db/connectDb');
|
||||
const { getBalanceConfig } = require('~/server/services/Config');
|
||||
|
||||
const domains = {
|
||||
client: process.env.DOMAIN_CLIENT,
|
||||
|
|
@ -24,14 +39,13 @@ const genericVerificationMessage = 'Please check your email to verify your email
|
|||
* @returns
|
||||
*/
|
||||
const logoutUser = async (req, refreshToken) => {
|
||||
const { Session } = db.models;
|
||||
try {
|
||||
const userId = req.user._id;
|
||||
const session = await Session.findSession({ userId: userId, refreshToken });
|
||||
const session = await findSession({ userId: userId, refreshToken });
|
||||
|
||||
if (session) {
|
||||
try {
|
||||
await Session.deleteSession({ sessionId: session._id });
|
||||
await deleteSession({ sessionId: session._id });
|
||||
} catch (deleteErr) {
|
||||
logger.error('[logoutUser] Failed to delete session.', deleteErr);
|
||||
return { status: 500, message: 'Failed to delete session.' };
|
||||
|
|
@ -83,7 +97,7 @@ const sendVerificationEmail = async (user) => {
|
|||
template: 'verifyEmail.handlebars',
|
||||
});
|
||||
|
||||
await db.models.Token.createToken({
|
||||
await createToken({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: hash,
|
||||
|
|
@ -102,8 +116,7 @@ const verifyEmail = async (req) => {
|
|||
const { email, token } = req.body;
|
||||
const decodedEmail = decodeURIComponent(email);
|
||||
|
||||
const { User, Token } = db.models;
|
||||
const user = await User.findUser({ email: decodedEmail }, 'email _id emailVerified');
|
||||
const user = await findUser({ email: decodedEmail }, 'email _id emailVerified');
|
||||
|
||||
if (!user) {
|
||||
logger.warn(`[verifyEmail] [User not found] [Email: ${decodedEmail}]`);
|
||||
|
|
@ -115,7 +128,7 @@ const verifyEmail = async (req) => {
|
|||
return { message: 'Email already verified', status: 'success' };
|
||||
}
|
||||
|
||||
let emailVerificationData = await Token.findToken({ email: decodedEmail });
|
||||
let emailVerificationData = await findToken({ email: decodedEmail });
|
||||
|
||||
if (!emailVerificationData) {
|
||||
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`);
|
||||
|
|
@ -131,16 +144,18 @@ const verifyEmail = async (req) => {
|
|||
return new Error('Invalid or expired email verification token');
|
||||
}
|
||||
|
||||
const updatedUser = await User.updateUser(emailVerificationData.userId, { emailVerified: true });
|
||||
const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true });
|
||||
|
||||
if (!updatedUser) {
|
||||
logger.warn(`[verifyEmail] [User update failed] [Email: ${decodedEmail}]`);
|
||||
return new Error('Failed to update user verification status');
|
||||
}
|
||||
|
||||
await Token.deleteTokens({ token: emailVerificationData.token });
|
||||
await deleteTokens({ token: emailVerificationData.token });
|
||||
logger.info(`[verifyEmail] Email verification successful [Email: ${decodedEmail}]`);
|
||||
return { message: 'Email verification was successful', status: 'success' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
* @param {MongoUser} user <email, password, name, username>
|
||||
|
|
@ -148,7 +163,6 @@ const verifyEmail = async (req) => {
|
|||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
||||
*/
|
||||
const registerUser = async (user, additionalData = {}) => {
|
||||
const { User } = db.models;
|
||||
const { error } = registerSchema.safeParse(user);
|
||||
if (error) {
|
||||
const errorMessage = errorsToString(error.errors);
|
||||
|
|
@ -165,7 +179,7 @@ const registerUser = async (user, additionalData = {}) => {
|
|||
|
||||
let newUserId;
|
||||
try {
|
||||
const existingUser = await User.findUser({ email }, 'email _id');
|
||||
const existingUser = await findUser({ email }, 'email _id');
|
||||
|
||||
if (existingUser) {
|
||||
logger.info(
|
||||
|
|
@ -187,7 +201,7 @@ const registerUser = async (user, additionalData = {}) => {
|
|||
}
|
||||
|
||||
//determine if this is the first registered user (not counting anonymous_user)
|
||||
const isFirstRegisteredUser = (await User.countUsers()) === 0;
|
||||
const isFirstRegisteredUser = (await countUsers()) === 0;
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const newUserData = {
|
||||
|
|
@ -205,7 +219,7 @@ const registerUser = async (user, additionalData = {}) => {
|
|||
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
||||
const balanceConfig = await getBalanceConfig();
|
||||
|
||||
const newUser = await User.createUser(newUserData, balanceConfig, disableTTL, true);
|
||||
const newUser = await createUser(newUserData, balanceConfig, disableTTL, true);
|
||||
newUserId = newUser._id;
|
||||
if (emailEnabled && !newUser.emailVerified) {
|
||||
await sendVerificationEmail({
|
||||
|
|
@ -214,14 +228,14 @@ const registerUser = async (user, additionalData = {}) => {
|
|||
name,
|
||||
});
|
||||
} else {
|
||||
await User.updateUser(newUserId, { emailVerified: true });
|
||||
await updateUser(newUserId, { emailVerified: true });
|
||||
}
|
||||
|
||||
return { status: 200, message: genericVerificationMessage };
|
||||
} catch (err) {
|
||||
logger.error('[registerUser] Error in registering user:', err);
|
||||
if (newUserId) {
|
||||
const result = await User.deleteUserById(newUserId);
|
||||
const result = await deleteUserById(newUserId);
|
||||
logger.warn(
|
||||
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
|
||||
);
|
||||
|
|
@ -236,8 +250,7 @@ const registerUser = async (user, additionalData = {}) => {
|
|||
*/
|
||||
const requestPasswordReset = async (req) => {
|
||||
const { email } = req.body;
|
||||
const { User, Token } = db.models;
|
||||
const user = await User.findUser({ email }, 'email _id');
|
||||
const user = await findUser({ email }, 'email _id');
|
||||
const emailEnabled = checkEmailConfig();
|
||||
|
||||
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
|
||||
|
|
@ -249,11 +262,11 @@ const requestPasswordReset = async (req) => {
|
|||
};
|
||||
}
|
||||
|
||||
await Token.deleteTokens({ userId: user._id });
|
||||
await deleteTokens({ userId: user._id });
|
||||
|
||||
const [resetToken, hash] = createTokenHash();
|
||||
|
||||
await Token.createToken({
|
||||
await createToken({
|
||||
userId: user._id,
|
||||
token: hash,
|
||||
createdAt: Date.now(),
|
||||
|
|
@ -298,8 +311,7 @@ const requestPasswordReset = async (req) => {
|
|||
* @returns
|
||||
*/
|
||||
const resetPassword = async (userId, token, password) => {
|
||||
const { User, Token } = db.models;
|
||||
let passwordResetToken = await Token.findToken({
|
||||
let passwordResetToken = await findToken({
|
||||
userId,
|
||||
});
|
||||
|
||||
|
|
@ -314,7 +326,7 @@ const resetPassword = async (userId, token, password) => {
|
|||
}
|
||||
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
const user = await User.updateUser(userId, { password: hash });
|
||||
const user = await updateUser(userId, { password: hash });
|
||||
|
||||
if (checkEmailConfig()) {
|
||||
await sendEmail({
|
||||
|
|
@ -329,7 +341,7 @@ const resetPassword = async (userId, token, password) => {
|
|||
});
|
||||
}
|
||||
|
||||
await Token.deleteTokens({ token: passwordResetToken.token });
|
||||
await deleteTokens({ token: passwordResetToken.token });
|
||||
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
|
||||
return { message: 'Password reset was successful' };
|
||||
};
|
||||
|
|
@ -344,20 +356,19 @@ const resetPassword = async (userId, token, password) => {
|
|||
*/
|
||||
const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||
try {
|
||||
const { User, Session } = db.models;
|
||||
const user = await User.getUserById(userId);
|
||||
const token = await User.generateToken(user);
|
||||
const user = await getUserById(userId);
|
||||
const token = await generateToken(user);
|
||||
|
||||
let session;
|
||||
let refreshToken;
|
||||
let refreshTokenExpires;
|
||||
|
||||
if (sessionId) {
|
||||
session = await Session.findSession({ sessionId: sessionId }, { lean: false });
|
||||
session = await findSession({ sessionId: sessionId }, { lean: false });
|
||||
refreshTokenExpires = session.expiration.getTime();
|
||||
refreshToken = await Session.generateRefreshToken(session);
|
||||
refreshToken = await generateRefreshToken(session);
|
||||
} else {
|
||||
const result = await Session.createSession(userId);
|
||||
const result = await createSession(userId);
|
||||
session = result.session;
|
||||
refreshToken = result.refreshToken;
|
||||
refreshTokenExpires = session.expiration.getTime();
|
||||
|
|
@ -381,6 +392,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function setOpenIDAuthTokens
|
||||
* Set OpenID Authentication Tokens
|
||||
|
|
@ -436,9 +448,8 @@ const setOpenIDAuthTokens = (tokenset, res) => {
|
|||
const resendVerificationEmail = async (req) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
const { User, Token } = db.models;
|
||||
await Token.deleteTokens(email);
|
||||
const user = await User.findUser({ email }, 'email _id name');
|
||||
await deleteTokens(email);
|
||||
const user = await findUser({ email }, 'email _id name');
|
||||
|
||||
if (!user) {
|
||||
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
||||
|
|
@ -463,7 +474,7 @@ const resendVerificationEmail = async (req) => {
|
|||
template: 'verifyEmail.handlebars',
|
||||
});
|
||||
|
||||
await Token.createToken({
|
||||
await createToken({
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
token: hash,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { connectDb, getModels } from '@librechat/backend/lib/db/connectDb';
|
||||
import { deleteMessages, deleteConvos, User, Balance } from '@librechat/backend/models';
|
||||
import { Transaction } from '@librechat/backend/models/Transaction';
|
||||
import { findUser, deleteAllUserSessions } from '@librechat/data-schemas';
|
||||
import { deleteMessages, deleteConvos } from '@librechat/backend/models';
|
||||
|
||||
type TUser = { email: string; password: string };
|
||||
|
||||
export default async function cleanupUser(user: TUser) {
|
||||
|
|
@ -10,29 +11,38 @@ export default async function cleanupUser(user: TUser) {
|
|||
const db = await connectDb();
|
||||
console.log('🤖: ✅ Connected to Database');
|
||||
|
||||
const { _id: user } = await User.findOne({ email }).lean();
|
||||
const foundUser = await findUser({ email });
|
||||
if (!foundUser) {
|
||||
console.log('🤖: ⚠️ User not found in Database');
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = foundUser._id;
|
||||
console.log('🤖: ✅ Found user in Database');
|
||||
|
||||
// Delete all conversations & associated messages
|
||||
const { deletedCount, messages } = await deleteConvos(user, {});
|
||||
const { deletedCount, messages } = await deleteConvos(userId, {});
|
||||
|
||||
if (messages.deletedCount > 0 || deletedCount > 0) {
|
||||
console.log(`🤖: ✅ Deleted ${deletedCount} convos & ${messages.deletedCount} messages`);
|
||||
}
|
||||
|
||||
// Ensure all user messages are deleted
|
||||
const { deletedCount: deletedMessages } = await deleteMessages({ user });
|
||||
const { deletedCount: deletedMessages } = await deleteMessages({ user: userId });
|
||||
if (deletedMessages > 0) {
|
||||
console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`);
|
||||
}
|
||||
|
||||
// TODO: fix this to delete all user sessions with the user's email
|
||||
const { User, Session } = getModels();
|
||||
await Session.deleteAllUserSessions(user);
|
||||
// Delete all user sessions
|
||||
await deleteAllUserSessions(userId.toString());
|
||||
|
||||
await User.deleteMany({ _id: user });
|
||||
await Balance.deleteMany({ user });
|
||||
await Transaction.deleteMany({ user });
|
||||
// Get models from the registered models
|
||||
const { User, Balance, Transaction } = getModels();
|
||||
|
||||
// Delete user, balance, and transactions using the registered models
|
||||
await User.deleteMany({ _id: userId });
|
||||
await Balance.deleteMany({ user: userId });
|
||||
await Transaction.deleteMany({ user: userId });
|
||||
|
||||
console.log('🤖: ✅ Deleted user from Database');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,20 @@
|
|||
// Export all types
|
||||
export * from './types';
|
||||
|
||||
// Export all models
|
||||
export * from './models';
|
||||
|
||||
// Export all methods
|
||||
export * from './methods';
|
||||
|
||||
// Export schemas (if needed for direct access)
|
||||
export { default as userSchema } from './schema/user';
|
||||
export { default as sessionSchema } from './schema/session';
|
||||
export { default as tokenSchema } from './schema/token';
|
||||
|
||||
// Export utility functions from schemas
|
||||
export { signPayload, hashToken } from './schema/session';
|
||||
|
||||
export { default as actionSchema } from './schema/action';
|
||||
export type { IAction } from './schema/action';
|
||||
|
||||
|
|
@ -49,21 +66,11 @@ export type { IPromptGroup, IPromptGroupDocument } from './schema/promptGroup';
|
|||
export { default as roleSchema } from './schema/role';
|
||||
export type { IRole } from './schema/role';
|
||||
|
||||
export { default as sessionSchema } from './schema/session';
|
||||
export type { ISession } from './schema/session';
|
||||
|
||||
export { default as shareSchema } from './schema/share';
|
||||
export type { ISharedLink } from './schema/share';
|
||||
|
||||
export { default as tokenSchema } from './schema/token';
|
||||
export type { IToken } from './schema/token';
|
||||
|
||||
export { default as toolCallSchema } from './schema/toolCall';
|
||||
export type { IToolCallData } from './schema/toolCall';
|
||||
|
||||
export { default as transactionSchema } from './schema/transaction';
|
||||
export type { ITransaction } from './schema/transaction';
|
||||
|
||||
export { default as userSchema } from './schema/user';
|
||||
export type { IUser } from './schema/user';
|
||||
export { registerModels } from './models';
|
||||
|
|
|
|||
97
packages/data-schemas/src/methods/README.md
Normal file
97
packages/data-schemas/src/methods/README.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Methods
|
||||
|
||||
This directory contains pure functions that replace the static methods from the schema files. This refactoring improves testability, type safety, and code modularity.
|
||||
|
||||
## Structure
|
||||
|
||||
- `userMethods.ts` - Functions for user operations
|
||||
- `sessionMethods.ts` - Functions for session operations
|
||||
- `tokenMethods.ts` - Functions for token operations
|
||||
- `index.ts` - Exports all methods for convenient importing
|
||||
|
||||
## Migration from Static Methods
|
||||
|
||||
Instead of calling static methods on models:
|
||||
|
||||
```typescript
|
||||
// OLD: Using static methods
|
||||
const user = await UserModel.findUser({ email: 'test@example.com' });
|
||||
const result = await UserModel.deleteUserById(userId);
|
||||
```
|
||||
|
||||
Use the pure functions with the model as the first parameter:
|
||||
|
||||
```typescript
|
||||
// NEW: Using pure functions
|
||||
import { findUser, deleteUserById } from '~/methods';
|
||||
import UserModel from '~/schema/user';
|
||||
|
||||
const user = await findUser(UserModel, { email: 'test@example.com' });
|
||||
const result = await deleteUserById(UserModel, userId);
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Pure Functions**: Methods are now side-effect free and testable
|
||||
2. **Better Types**: Proper TypeScript typing throughout
|
||||
3. **Dependency Injection**: Models are passed as parameters
|
||||
4. **Modular**: Functions can be imported individually or as a group
|
||||
5. **No Magic**: Clear explicit dependencies
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### User Methods
|
||||
|
||||
```typescript
|
||||
import { createUser, findUser, updateUser } from '~/methods';
|
||||
import UserModel from '~/schema/user';
|
||||
|
||||
// Create a user
|
||||
const newUser = await createUser(
|
||||
UserModel,
|
||||
{ email: 'user@example.com', name: 'John' },
|
||||
{ enabled: true, startBalance: 100 }
|
||||
);
|
||||
|
||||
// Find a user
|
||||
const user = await findUser(UserModel, { email: 'user@example.com' });
|
||||
|
||||
// Update a user
|
||||
const updated = await updateUser(UserModel, userId, { name: 'Jane' });
|
||||
```
|
||||
|
||||
### Session Methods
|
||||
|
||||
```typescript
|
||||
import { createSession, findSession, deleteSession } from '~/methods';
|
||||
import SessionModel from '~/schema/session';
|
||||
|
||||
// Create session
|
||||
const { session, refreshToken } = await createSession(SessionModel, userId);
|
||||
|
||||
// Find session
|
||||
const foundSession = await findSession(SessionModel, { refreshToken });
|
||||
|
||||
// Delete session
|
||||
await deleteSession(SessionModel, { sessionId });
|
||||
```
|
||||
|
||||
### Token Methods
|
||||
|
||||
```typescript
|
||||
import { createToken, findToken, deleteTokens } from '~/methods';
|
||||
import TokenModel from '~/schema/token';
|
||||
|
||||
// Create token
|
||||
const token = await createToken(TokenModel, {
|
||||
userId,
|
||||
token: 'abc123',
|
||||
expiresIn: 3600
|
||||
});
|
||||
|
||||
// Find token
|
||||
const foundToken = await findToken(TokenModel, { token: 'abc123' });
|
||||
|
||||
// Delete tokens
|
||||
await deleteTokens(TokenModel, { userId });
|
||||
```
|
||||
8
packages/data-schemas/src/methods/index.ts
Normal file
8
packages/data-schemas/src/methods/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// User methods
|
||||
export * from './user';
|
||||
|
||||
// Session methods
|
||||
export * from './session';
|
||||
|
||||
// Token methods
|
||||
export * from './token';
|
||||
202
packages/data-schemas/src/methods/session.ts
Normal file
202
packages/data-schemas/src/methods/session.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import mongoose from 'mongoose';
|
||||
import { Session } from '~/models';
|
||||
import {
|
||||
ISession,
|
||||
CreateSessionOptions,
|
||||
SessionSearchParams,
|
||||
SessionQueryOptions,
|
||||
DeleteSessionParams,
|
||||
DeleteAllSessionsOptions,
|
||||
SessionResult,
|
||||
SessionError,
|
||||
} from '~/types';
|
||||
import { signPayload, hashToken } from '~/schema/session';
|
||||
import logger from '~/config/winston';
|
||||
|
||||
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
||||
const expires = eval(REFRESH_TOKEN_EXPIRY ?? '0') ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||
|
||||
/**
|
||||
* Creates a new session for a user
|
||||
*/
|
||||
export async function createSession(
|
||||
userId: string,
|
||||
options: CreateSessionOptions = {},
|
||||
): Promise<SessionResult> {
|
||||
if (!userId) {
|
||||
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||
}
|
||||
|
||||
try {
|
||||
const session = {
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
user: new mongoose.Types.ObjectId(userId),
|
||||
expiration: options.expiration || new Date(Date.now() + expires),
|
||||
};
|
||||
const refreshToken = await generateRefreshToken(session);
|
||||
|
||||
return { session, refreshToken };
|
||||
} catch (error) {
|
||||
logger.error('[createSession] Error creating session:', error);
|
||||
throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a session by various parameters
|
||||
*/
|
||||
export async function findSession(
|
||||
params: SessionSearchParams,
|
||||
options: SessionQueryOptions = { lean: true },
|
||||
): Promise<ISession | null> {
|
||||
try {
|
||||
const query: Record<string, unknown> = {};
|
||||
|
||||
if (!params.refreshToken && !params.userId && !params.sessionId) {
|
||||
throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
|
||||
}
|
||||
|
||||
if (params.refreshToken) {
|
||||
const tokenHash = await hashToken(params.refreshToken);
|
||||
query.refreshTokenHash = tokenHash;
|
||||
}
|
||||
|
||||
if (params.userId) {
|
||||
query.user = params.userId;
|
||||
}
|
||||
|
||||
if (params.sessionId) {
|
||||
const sessionId =
|
||||
typeof params.sessionId === 'object' && 'sessionId' in params.sessionId
|
||||
? params.sessionId.sessionId
|
||||
: params.sessionId;
|
||||
if (!mongoose.Types.ObjectId.isValid(sessionId)) {
|
||||
throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
|
||||
}
|
||||
query._id = sessionId;
|
||||
}
|
||||
|
||||
// Add expiration check to only return valid sessions
|
||||
query.expiration = { $gt: new Date() };
|
||||
|
||||
const sessionQuery = Session.findOne(query);
|
||||
|
||||
if (options.lean) {
|
||||
return await sessionQuery.lean();
|
||||
}
|
||||
|
||||
return await sessionQuery.exec();
|
||||
} catch (error) {
|
||||
logger.error('[findSession] Error finding session:', error);
|
||||
throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a session by refresh token or session ID
|
||||
*/
|
||||
export async function deleteSession(
|
||||
params: DeleteSessionParams,
|
||||
): Promise<{ deletedCount?: number }> {
|
||||
try {
|
||||
if (!params.refreshToken && !params.sessionId) {
|
||||
throw new SessionError(
|
||||
'Either refreshToken or sessionId is required',
|
||||
'INVALID_DELETE_PARAMS',
|
||||
);
|
||||
}
|
||||
|
||||
const query: Record<string, unknown> = {};
|
||||
|
||||
if (params.refreshToken) {
|
||||
query.refreshTokenHash = await hashToken(params.refreshToken);
|
||||
}
|
||||
|
||||
if (params.sessionId) {
|
||||
query._id = params.sessionId;
|
||||
}
|
||||
|
||||
const result = await Session.deleteOne(query);
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
logger.warn('[deleteSession] No session found to delete');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[deleteSession] Error deleting session:', error);
|
||||
throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a refresh token for a session
|
||||
*/
|
||||
export async function generateRefreshToken(session: Partial<ISession>): Promise<string> {
|
||||
if (!session || !session.user) {
|
||||
throw new SessionError('Invalid session object', 'INVALID_SESSION');
|
||||
}
|
||||
|
||||
try {
|
||||
const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
|
||||
if (!session.expiration) {
|
||||
session.expiration = new Date(expiresIn);
|
||||
}
|
||||
|
||||
const refreshToken = await signPayload({
|
||||
payload: {
|
||||
id: session.user,
|
||||
sessionId: session._id,
|
||||
},
|
||||
secret: process.env.JWT_REFRESH_SECRET,
|
||||
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
|
||||
});
|
||||
|
||||
session.refreshTokenHash = await hashToken(refreshToken);
|
||||
await Session.create(session);
|
||||
return refreshToken;
|
||||
} catch (error) {
|
||||
logger.error('[generateRefreshToken] Error generating refresh token:', error);
|
||||
throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all sessions for a user
|
||||
*/
|
||||
export async function deleteAllUserSessions(
|
||||
userId: string | { userId: string },
|
||||
options: DeleteAllSessionsOptions = {},
|
||||
): Promise<{ deletedCount?: number }> {
|
||||
try {
|
||||
if (!userId) {
|
||||
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||
}
|
||||
|
||||
// Extract userId if it's passed as an object
|
||||
const userIdString = typeof userId === 'object' ? userId.userId : userId;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(userIdString)) {
|
||||
throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
|
||||
}
|
||||
|
||||
const query: Record<string, unknown> = { user: userIdString };
|
||||
|
||||
if (options.excludeCurrentSession && options.currentSessionId) {
|
||||
query._id = { $ne: options.currentSessionId };
|
||||
}
|
||||
|
||||
const result = await Session.deleteMany(query);
|
||||
|
||||
if (result.deletedCount && result.deletedCount > 0) {
|
||||
logger.debug(
|
||||
`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
|
||||
throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
|
||||
}
|
||||
}
|
||||
105
packages/data-schemas/src/methods/token.ts
Normal file
105
packages/data-schemas/src/methods/token.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { Token } from '~/models';
|
||||
import { IToken, TokenCreateData, TokenQuery, TokenUpdateData, TokenDeleteResult } from '~/types';
|
||||
import logger from '~/config/winston';
|
||||
|
||||
/**
|
||||
* Creates a new Token instance.
|
||||
*/
|
||||
export async function createToken(tokenData: TokenCreateData): Promise<IToken> {
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000);
|
||||
|
||||
const newTokenData = {
|
||||
...tokenData,
|
||||
createdAt: currentTime,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
return await Token.create(newTokenData);
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while creating token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a Token document that matches the provided query.
|
||||
*/
|
||||
export async function updateToken(
|
||||
query: TokenQuery,
|
||||
updateData: TokenUpdateData,
|
||||
): Promise<IToken | null> {
|
||||
try {
|
||||
return await Token.findOneAndUpdate(query, updateData, { new: true });
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while updating token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all Token documents that match the provided token, user ID, or email.
|
||||
*/
|
||||
export async function deleteTokens(query: TokenQuery): Promise<TokenDeleteResult> {
|
||||
try {
|
||||
const conditions = [];
|
||||
|
||||
if (query.userId) {
|
||||
conditions.push({ userId: query.userId });
|
||||
}
|
||||
if (query.token) {
|
||||
conditions.push({ token: query.token });
|
||||
}
|
||||
if (query.email) {
|
||||
conditions.push({ email: query.email });
|
||||
}
|
||||
if (query.identifier) {
|
||||
conditions.push({ identifier: query.identifier });
|
||||
}
|
||||
|
||||
if (conditions.length === 0) {
|
||||
throw new Error('At least one query parameter must be provided');
|
||||
}
|
||||
|
||||
return await Token.deleteMany({
|
||||
$or: conditions,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while deleting tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a Token document that matches the provided query.
|
||||
*/
|
||||
export async function findToken(query: TokenQuery): Promise<IToken | null> {
|
||||
try {
|
||||
const conditions = [];
|
||||
|
||||
if (query.userId) {
|
||||
conditions.push({ userId: query.userId });
|
||||
}
|
||||
if (query.token) {
|
||||
conditions.push({ token: query.token });
|
||||
}
|
||||
if (query.email) {
|
||||
conditions.push({ email: query.email });
|
||||
}
|
||||
if (query.identifier) {
|
||||
conditions.push({ identifier: query.identifier });
|
||||
}
|
||||
|
||||
if (conditions.length === 0) {
|
||||
throw new Error('At least one query parameter must be provided');
|
||||
}
|
||||
|
||||
return (await Token.findOne({
|
||||
$and: conditions,
|
||||
}).lean()) as IToken | null;
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while finding token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
151
packages/data-schemas/src/methods/user.ts
Normal file
151
packages/data-schemas/src/methods/user.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import mongoose, { FilterQuery } from 'mongoose';
|
||||
import { User, Balance } from '~/models';
|
||||
import { IUser, BalanceConfig, UserCreateData, UserUpdateResult } from '~/types';
|
||||
import { signPayload } from '~/schema/session';
|
||||
|
||||
/**
|
||||
* Search for a single user based on partial data and return matching user document as plain object.
|
||||
*/
|
||||
export async function findUser(
|
||||
searchCriteria: FilterQuery<IUser>,
|
||||
fieldsToSelect?: string | string[] | null,
|
||||
): Promise<IUser | null> {
|
||||
const query = User.findOne(searchCriteria);
|
||||
if (fieldsToSelect) {
|
||||
query.select(fieldsToSelect);
|
||||
}
|
||||
return await query.lean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of user documents in the collection based on the provided filter.
|
||||
*/
|
||||
export async function countUsers(filter: FilterQuery<IUser> = {}): Promise<number> {
|
||||
return await User.countDocuments(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user, optionally with a TTL of 1 week.
|
||||
*/
|
||||
export async function createUser(
|
||||
data: UserCreateData,
|
||||
balanceConfig?: BalanceConfig,
|
||||
disableTTL: boolean = true,
|
||||
returnUser: boolean = false,
|
||||
): Promise<mongoose.Types.ObjectId | Partial<IUser>> {
|
||||
const userData: Partial<IUser> = {
|
||||
...data,
|
||||
expiresAt: disableTTL ? undefined : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds
|
||||
};
|
||||
|
||||
if (disableTTL) {
|
||||
delete userData.expiresAt;
|
||||
}
|
||||
|
||||
const user = await User.create(userData);
|
||||
|
||||
// If balance is enabled, create or update a balance record for the user
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user with new data without overwriting existing properties.
|
||||
*/
|
||||
export async function updateUser(
|
||||
userId: string,
|
||||
updateData: Partial<IUser>,
|
||||
): Promise<IUser | null> {
|
||||
const updateOperation = {
|
||||
$set: updateData,
|
||||
$unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL
|
||||
};
|
||||
return await User.findByIdAndUpdate(userId, updateOperation, {
|
||||
new: true,
|
||||
runValidators: true,
|
||||
}).lean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by ID and convert the found user document to a plain object.
|
||||
*/
|
||||
export async function getUserById(
|
||||
userId: string,
|
||||
fieldsToSelect?: string | string[] | null,
|
||||
): Promise<IUser | null> {
|
||||
const query = User.findById(userId);
|
||||
if (fieldsToSelect) {
|
||||
query.select(fieldsToSelect);
|
||||
}
|
||||
return await query.lean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user by their unique ID.
|
||||
*/
|
||||
export async function deleteUserById(userId: string): Promise<UserUpdateResult> {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a JWT token for a given user.
|
||||
*/
|
||||
export async function generateToken(user: IUser): Promise<string> {
|
||||
if (!user) {
|
||||
throw new Error('No user provided');
|
||||
}
|
||||
|
||||
const expires = eval(process.env.SESSION_EXPIRY ?? '0') ?? 1000 * 60 * 15;
|
||||
|
||||
return await signPayload({
|
||||
payload: {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
provider: user.provider,
|
||||
email: user.email,
|
||||
},
|
||||
secret: process.env.JWT_SECRET,
|
||||
expirationTime: expires / 1000,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,185 +1,25 @@
|
|||
import type { Mongoose } from 'mongoose';
|
||||
import { default as actionSchema } from '../schema/action';
|
||||
import { default as agentSchema } from '../schema/agent';
|
||||
import { default as assistantSchema } from '../schema/assistant';
|
||||
import { default as balanceSchema } from '../schema/balance';
|
||||
import { default as bannerSchema } from '../schema/banner';
|
||||
import { default as categoriesSchema } from '../schema/categories';
|
||||
import { default as conversationTagSchema } from '../schema/conversationTag';
|
||||
import { default as convoSchema } from '../schema/convo';
|
||||
import { default as fileSchema } from '../schema/file';
|
||||
import { default as keySchema } from '../schema/key';
|
||||
import { default as messageSchema } from '../schema/message';
|
||||
import { default as pluginAuthSchema } from '../schema/pluginAuth';
|
||||
import { default as presetSchema } from '../schema/preset';
|
||||
import { default as projectSchema } from '../schema/project';
|
||||
import { default as promptSchema } from '../schema/prompt';
|
||||
import { default as promptGroupSchema } from '../schema/promptGroup';
|
||||
import { default as roleSchema } from '../schema/role';
|
||||
import { default as sessionSchema } from '../schema/session';
|
||||
import { default as shareSchema } from '../schema/share';
|
||||
import { default as tokenSchema } from '../schema/token';
|
||||
import { default as toolCallSchema } from '../schema/toolCall';
|
||||
import { default as transactionSchema } from '../schema/transaction';
|
||||
import { default as userSchema } from '../schema/user';
|
||||
import mongoMeili from './plugins/mongoMeili';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export const registerModels = (mongoose: Mongoose) => {
|
||||
const User = registerUserModel(mongoose);
|
||||
const Session = registerSessionModel(mongoose);
|
||||
const Token = registerTokenModel(mongoose);
|
||||
const Message = registerMessageModel(mongoose);
|
||||
const Action = registerActionModel(mongoose);
|
||||
const Agent = registerAgentModel(mongoose);
|
||||
const Assistant = registerAssistantModel(mongoose);
|
||||
const Balance = registerBalanceModel(mongoose);
|
||||
const Banner = registerBannerModel(mongoose);
|
||||
const Categories = registerCategoriesModel(mongoose);
|
||||
const ConversationTag = registerConversationTagModel(mongoose);
|
||||
const File = registerFileModel(mongoose);
|
||||
const Key = registerKeyModel(mongoose);
|
||||
const PluginAuth = registerPluginAuthModel(mongoose);
|
||||
const Preset = registerPresetModel(mongoose);
|
||||
const Project = registerProjectModel(mongoose);
|
||||
const Prompt = registerPromptModel(mongoose);
|
||||
const PromptGroup = registerPromptGroupModel(mongoose);
|
||||
const Role = registerRoleModel(mongoose);
|
||||
const SharedLink = registerShareModel(mongoose);
|
||||
const ToolCall = registerToolCallModel(mongoose);
|
||||
const Transaction = registerTransactionModel(mongoose);
|
||||
const Conversation = registerConversationModel(mongoose);
|
||||
// Import schemas
|
||||
import userSchema from '~/schema/user';
|
||||
import sessionSchema from '~/schema/session';
|
||||
import tokenSchema from '~/schema/token';
|
||||
import balanceSchema from '~/schema/balance';
|
||||
|
||||
return {
|
||||
// Import types
|
||||
import { IUser, ISession, IToken } from '~/types';
|
||||
import { IBalance } from '~/schema/balance';
|
||||
|
||||
// Create and export model instances
|
||||
export const User = mongoose.model<IUser>('User', userSchema);
|
||||
export const Session = mongoose.model<ISession>('Session', sessionSchema);
|
||||
export const Token = mongoose.model<IToken>('Token', tokenSchema);
|
||||
export const Balance = mongoose.model<IBalance>('Balance', balanceSchema);
|
||||
|
||||
// Default export with all models
|
||||
export default {
|
||||
User,
|
||||
Session,
|
||||
Token,
|
||||
Message,
|
||||
Action,
|
||||
Agent,
|
||||
Assistant,
|
||||
Balance,
|
||||
Banner,
|
||||
Categories,
|
||||
ConversationTag,
|
||||
File,
|
||||
Key,
|
||||
PluginAuth,
|
||||
Preset,
|
||||
Project,
|
||||
Prompt,
|
||||
PromptGroup,
|
||||
Role,
|
||||
SharedLink,
|
||||
ToolCall,
|
||||
Transaction,
|
||||
Conversation,
|
||||
};
|
||||
};
|
||||
|
||||
const registerSessionModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Session || mongoose.model('Session', sessionSchema);
|
||||
};
|
||||
|
||||
const registerUserModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.User || mongoose.model('User', userSchema);
|
||||
};
|
||||
|
||||
const registerTokenModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Token || mongoose.model('Token', tokenSchema);
|
||||
};
|
||||
|
||||
const registerActionModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Action || mongoose.model('Action', actionSchema);
|
||||
};
|
||||
|
||||
const registerMessageModel = (mongoose: Mongoose) => {
|
||||
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
messageSchema.plugin(mongoMeili, {
|
||||
host: process.env.MEILI_HOST,
|
||||
apiKey: process.env.MEILI_MASTER_KEY,
|
||||
indexName: 'messages',
|
||||
primaryKey: 'messageId',
|
||||
});
|
||||
}
|
||||
|
||||
return mongoose.models.Message || mongoose.model('Message', messageSchema);
|
||||
};
|
||||
|
||||
const registerAgentModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Agent || mongoose.model('Agent', agentSchema);
|
||||
};
|
||||
|
||||
const registerAssistantModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Assistant || mongoose.model('Assistant', assistantSchema);
|
||||
};
|
||||
|
||||
const registerBalanceModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Balance || mongoose.model('Balance', balanceSchema);
|
||||
};
|
||||
|
||||
const registerBannerModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Banner || mongoose.model('Banner', bannerSchema);
|
||||
};
|
||||
|
||||
const registerCategoriesModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Categories || mongoose.model('Categories', categoriesSchema);
|
||||
};
|
||||
|
||||
const registerConversationTagModel = (mongoose: Mongoose) => {
|
||||
return (
|
||||
mongoose.models.ConversationTag || mongoose.model('ConversationTag', conversationTagSchema)
|
||||
);
|
||||
};
|
||||
const registerFileModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.File || mongoose.model('File', fileSchema);
|
||||
};
|
||||
|
||||
const registerKeyModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Key || mongoose.model('Key', keySchema);
|
||||
};
|
||||
|
||||
const registerPluginAuthModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.PluginAuth || mongoose.model('PluginAuth', pluginAuthSchema);
|
||||
};
|
||||
|
||||
const registerPresetModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Preset || mongoose.model('Preset', presetSchema);
|
||||
};
|
||||
|
||||
const registerProjectModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Project || mongoose.model('Project', projectSchema);
|
||||
};
|
||||
const registerPromptModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
|
||||
};
|
||||
const registerPromptGroupModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.PromptGroup || mongoose.model('PromptGroup', promptGroupSchema);
|
||||
};
|
||||
|
||||
const registerRoleModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Role || mongoose.model('Role', roleSchema);
|
||||
};
|
||||
const registerShareModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.SharedLink || mongoose.model('SharedLink', shareSchema);
|
||||
};
|
||||
|
||||
const registerToolCallModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.ToolCall || mongoose.model('ToolCall', toolCallSchema);
|
||||
};
|
||||
|
||||
const registerTransactionModel = (mongoose: Mongoose) => {
|
||||
return mongoose.models.Transaction || mongoose.model('Transaction', transactionSchema);
|
||||
};
|
||||
const registerConversationModel = (mongoose: Mongoose) => {
|
||||
if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||
convoSchema.plugin(mongoMeili, {
|
||||
host: process.env.MEILI_HOST,
|
||||
apiKey: process.env.MEILI_MASTER_KEY,
|
||||
/** Note: Will get created automatically if it doesn't exist already */
|
||||
indexName: 'convos',
|
||||
primaryKey: 'conversationId',
|
||||
});
|
||||
}
|
||||
|
||||
return mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import mongoose, { Schema, Document, Types } from 'mongoose';
|
||||
import mongoose, { Schema } from 'mongoose';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import logger from '../config/winston';
|
||||
const { webcrypto } = require('node:crypto');
|
||||
|
||||
export interface ISession extends Document {
|
||||
refreshTokenHash: string;
|
||||
expiration: Date;
|
||||
user: Types.ObjectId;
|
||||
}
|
||||
import { webcrypto } from 'node:crypto';
|
||||
import { ISession, SignPayloadParams } from '~/types';
|
||||
|
||||
const sessionSchema: Schema<ISession> = new Schema({
|
||||
refreshTokenHash: {
|
||||
|
|
@ -26,224 +20,18 @@ const sessionSchema: Schema<ISession> = new Schema({
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Error class for Session-related errors
|
||||
*/
|
||||
class SessionError extends Error {
|
||||
constructor(message, code = 'SESSION_ERROR') {
|
||||
super(message);
|
||||
this.name = 'SessionError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
||||
const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||
|
||||
/**
|
||||
* Creates a new session for a user
|
||||
* @param {string} userId - The ID of the user
|
||||
* @param {Object} options - Additional options for session creation
|
||||
* @param {Date} options.expiration - Custom expiration date
|
||||
* @returns {Promise<{session: Session, refreshToken: string}>}
|
||||
* @throws {SessionError}
|
||||
*/
|
||||
sessionSchema.statics.createSession = async function (userId, options = {}) {
|
||||
if (!userId) {
|
||||
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||
export async function signPayload({
|
||||
payload,
|
||||
secret,
|
||||
expirationTime,
|
||||
}: SignPayloadParams): Promise<string> {
|
||||
return jwt.sign(payload, secret!, { expiresIn: expirationTime });
|
||||
}
|
||||
|
||||
try {
|
||||
const session = {
|
||||
_id: new Types.ObjectId(),
|
||||
user: userId,
|
||||
expiration: options.expiration || new Date(Date.now() + expires),
|
||||
};
|
||||
const refreshToken = await this.generateRefreshToken(session);
|
||||
|
||||
return { session, refreshToken };
|
||||
} catch (error) {
|
||||
logger.error('[createSession] Error creating session:', error);
|
||||
throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a session by various parameters
|
||||
* @param {Object} params - Search parameters
|
||||
* @param {string} [params.refreshToken] - The refresh token to search by
|
||||
* @param {string} [params.userId] - The user ID to search by
|
||||
* @param {string} [params.sessionId] - The session ID to search by
|
||||
* @param {Object} [options] - Additional options
|
||||
* @param {boolean} [options.lean=true] - Whether to return plain objects instead of documents
|
||||
* @returns {Promise<Session|null>}
|
||||
* @throws {SessionError}
|
||||
*/
|
||||
sessionSchema.statics.findSession = async function (params, options = { lean: true }) {
|
||||
try {
|
||||
const query = {};
|
||||
|
||||
if (!params.refreshToken && !params.userId && !params.sessionId) {
|
||||
throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
|
||||
}
|
||||
|
||||
if (params.refreshToken) {
|
||||
const tokenHash = await hashToken(params.refreshToken);
|
||||
query.refreshTokenHash = tokenHash;
|
||||
}
|
||||
|
||||
if (params.userId) {
|
||||
query.user = params.userId;
|
||||
}
|
||||
|
||||
if (params.sessionId) {
|
||||
const sessionId = params.sessionId.sessionId || params.sessionId;
|
||||
if (!mongoose.Types.ObjectId.isValid(sessionId)) {
|
||||
throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
|
||||
}
|
||||
query._id = sessionId;
|
||||
}
|
||||
|
||||
// Add expiration check to only return valid sessions
|
||||
query.expiration = { $gt: new Date() };
|
||||
|
||||
const sessionQuery = this.findOne(query);
|
||||
|
||||
if (options.lean) {
|
||||
return await sessionQuery.lean();
|
||||
}
|
||||
|
||||
return await sessionQuery.exec();
|
||||
} catch (error) {
|
||||
logger.error('[findSession] Error finding session:', error);
|
||||
throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a session by refresh token or session ID
|
||||
* @param {Object} params - Delete parameters
|
||||
* @param {string} [params.refreshToken] - The refresh token of the session to delete
|
||||
* @param {string} [params.sessionId] - The ID of the session to delete
|
||||
* @returns {Promise<Object>}
|
||||
* @throws {SessionError}
|
||||
*/
|
||||
sessionSchema.statics.deleteSession = async function (params) {
|
||||
try {
|
||||
if (!params.refreshToken && !params.sessionId) {
|
||||
throw new SessionError(
|
||||
'Either refreshToken or sessionId is required',
|
||||
'INVALID_DELETE_PARAMS',
|
||||
);
|
||||
}
|
||||
|
||||
const query = {};
|
||||
|
||||
if (params.refreshToken) {
|
||||
query.refreshTokenHash = await hashToken(params.refreshToken);
|
||||
}
|
||||
|
||||
if (params.sessionId) {
|
||||
query._id = params.sessionId;
|
||||
}
|
||||
|
||||
const result = await this.deleteOne(query);
|
||||
|
||||
if (result.deletedCount === 0) {
|
||||
logger.warn('[deleteSession] No session found to delete');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[deleteSession] Error deleting session:', error);
|
||||
throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a refresh token for a session
|
||||
* @param {Session} session - The session to generate a token for
|
||||
* @returns {Promise<string>}
|
||||
* @throws {SessionError}
|
||||
*/
|
||||
sessionSchema.statics.generateRefreshToken = async function (session) {
|
||||
if (!session || !session.user) {
|
||||
throw new SessionError('Invalid session object', 'INVALID_SESSION');
|
||||
}
|
||||
|
||||
try {
|
||||
const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
|
||||
if (!session.expiration) {
|
||||
session.expiration = new Date(expiresIn);
|
||||
}
|
||||
|
||||
const refreshToken = await signPayload({
|
||||
payload: {
|
||||
id: session.user,
|
||||
sessionId: session._id,
|
||||
},
|
||||
secret: process.env.JWT_REFRESH_SECRET,
|
||||
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
|
||||
});
|
||||
|
||||
session.refreshTokenHash = await hashToken(refreshToken);
|
||||
await this.create(session);
|
||||
return refreshToken;
|
||||
} catch (error) {
|
||||
logger.error('[generateRefreshToken] Error generating refresh token:', error);
|
||||
throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes all sessions for a user
|
||||
* @param {string} userId - The ID of the user
|
||||
* @param {Object} [options] - Additional options
|
||||
* @param {boolean} [options.excludeCurrentSession] - Whether to exclude the current session
|
||||
* @param {string} [options.currentSessionId] - The ID of the current session to exclude
|
||||
* @returns {Promise<Object>}
|
||||
* @throws {SessionError}
|
||||
*/
|
||||
sessionSchema.statics.deleteAllUserSessions = async function (userId, options = {}) {
|
||||
try {
|
||||
if (!userId) {
|
||||
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||
}
|
||||
|
||||
// Extract userId if it's passed as an object
|
||||
const userIdString = userId.userId || userId;
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(userIdString)) {
|
||||
throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
|
||||
}
|
||||
|
||||
const query = { user: userIdString };
|
||||
|
||||
if (options.excludeCurrentSession && options.currentSessionId) {
|
||||
query._id = { $ne: options.currentSessionId };
|
||||
}
|
||||
|
||||
const result = await this.deleteMany(query);
|
||||
|
||||
if (result.deletedCount > 0) {
|
||||
logger.debug(
|
||||
`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
|
||||
throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
|
||||
}
|
||||
};
|
||||
|
||||
export async function signPayload({ payload, secret, expirationTime }) {
|
||||
return jwt.sign(payload, secret, { expiresIn: expirationTime });
|
||||
}
|
||||
|
||||
export async function hashToken(str) {
|
||||
export async function hashToken(str: string): Promise<string> {
|
||||
const data = new TextEncoder().encode(str);
|
||||
const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
|
||||
return Buffer.from(hashBuffer).toString('hex');
|
||||
}
|
||||
|
||||
export default sessionSchema;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,5 @@
|
|||
import { Schema, Document, Types } from 'mongoose';
|
||||
import { logger } from '~/config';
|
||||
|
||||
export interface IToken extends Document {
|
||||
userId: Types.ObjectId;
|
||||
email?: string;
|
||||
type?: string;
|
||||
identifier?: string;
|
||||
token: string;
|
||||
createdAt: Date;
|
||||
expiresAt: Date;
|
||||
metadata?: Map<string, unknown>;
|
||||
}
|
||||
import { Schema } from 'mongoose';
|
||||
import { IToken } from '~/types';
|
||||
|
||||
const tokenSchema: Schema<IToken> = new Schema({
|
||||
userId: {
|
||||
|
|
@ -48,116 +37,4 @@ const tokenSchema: Schema<IToken> = new Schema({
|
|||
|
||||
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
||||
|
||||
/**
|
||||
* Creates a new Token instance.
|
||||
* @param {Object} tokenData - The data for the new Token.
|
||||
* @param {mongoose.Types.ObjectId} tokenData.userId - The user's ID. It is required.
|
||||
* @param {String} tokenData.email - The user's email.
|
||||
* @param {String} tokenData.token - The token. It is required.
|
||||
* @param {Number} tokenData.expiresIn - The number of seconds until the token expires.
|
||||
* @returns {Promise<mongoose.Document>} The new Token instance.
|
||||
* @throws Will throw an error if token creation fails.
|
||||
*/
|
||||
tokenSchema.statics.createToken = async function (tokenData) {
|
||||
try {
|
||||
const currentTime = new Date();
|
||||
const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000);
|
||||
|
||||
const newTokenData = {
|
||||
...tokenData,
|
||||
createdAt: currentTime,
|
||||
expiresAt,
|
||||
};
|
||||
|
||||
return await this.create(newTokenData);
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while creating token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a Token document that matches the provided query.
|
||||
* @param {Object} query - The query to match against.
|
||||
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||
* @param {String} query.token - The token value.
|
||||
* @param {String} [query.email] - The email of the user.
|
||||
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
|
||||
* @param {Object} updateData - The data to update the Token with.
|
||||
* @returns {Promise<mongoose.Document|null>} The updated Token document, or null if not found.
|
||||
* @throws Will throw an error if the update operation fails.
|
||||
*/
|
||||
tokenSchema.statics.updateToken = async function (query, updateData) {
|
||||
try {
|
||||
return await this.findOneAndUpdate(query, updateData, { new: true });
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while updating token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes all Token documents that match the provided token, user ID, or email.
|
||||
* @param {Object} query - The query to match against.
|
||||
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||
* @param {String} query.token - The token value.
|
||||
* @param {String} [query.email] - The email of the user.
|
||||
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
|
||||
* @returns {Promise<Object>} The result of the delete operation.
|
||||
* @throws Will throw an error if the delete operation fails.
|
||||
*/
|
||||
tokenSchema.statics.deleteTokens = async function (query) {
|
||||
try {
|
||||
return await Token.deleteMany({
|
||||
$or: [
|
||||
{ userId: query.userId },
|
||||
{ token: query.token },
|
||||
{ email: query.email },
|
||||
{ identifier: query.identifier },
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while deleting tokens:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a Token document that matches the provided query.
|
||||
* @param {Object} query - The query to match against.
|
||||
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
|
||||
* @param {String} query.token - The token value.
|
||||
* @param {String} [query.email] - The email of the user.
|
||||
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
|
||||
* @returns {Promise<Object|null>} The matched Token document, or null if not found.
|
||||
* @throws Will throw an error if the find operation fails.
|
||||
*/
|
||||
tokenSchema.statics.findToken = async function (query) {
|
||||
try {
|
||||
const conditions = [];
|
||||
|
||||
if (query.userId) {
|
||||
conditions.push({ userId: query.userId });
|
||||
}
|
||||
if (query.token) {
|
||||
conditions.push({ token: query.token });
|
||||
}
|
||||
if (query.email) {
|
||||
conditions.push({ email: query.email });
|
||||
}
|
||||
if (query.identifier) {
|
||||
conditions.push({ identifier: query.identifier });
|
||||
}
|
||||
|
||||
const token = await this.findOne({
|
||||
$and: conditions,
|
||||
}).lean();
|
||||
|
||||
return token;
|
||||
} catch (error) {
|
||||
logger.debug('An error occurred while finding token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default tokenSchema;
|
||||
|
|
|
|||
|
|
@ -1,40 +1,6 @@
|
|||
import mongoose, { Schema, Document, Model, Types } from 'mongoose';
|
||||
import { Schema } from 'mongoose';
|
||||
import { SystemRoles } from 'librechat-data-provider';
|
||||
import { default as balanceSchema } from './balance';
|
||||
import { signPayload } from './session';
|
||||
export interface IUser extends Document {
|
||||
name?: string;
|
||||
username?: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
provider: string;
|
||||
role?: string;
|
||||
googleId?: string;
|
||||
facebookId?: string;
|
||||
openidId?: string;
|
||||
samlId?: string;
|
||||
ldapId?: string;
|
||||
githubId?: string;
|
||||
discordId?: string;
|
||||
appleId?: string;
|
||||
plugins?: unknown[];
|
||||
twoFactorEnabled?: boolean;
|
||||
totpSecret?: string;
|
||||
backupCodes?: Array<{
|
||||
codeHash: string;
|
||||
used: boolean;
|
||||
usedAt?: Date | null;
|
||||
}>;
|
||||
refreshToken?: Array<{
|
||||
refreshToken: string;
|
||||
}>;
|
||||
expiresAt?: Date;
|
||||
termsAccepted?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
import { IUser } from '~/types';
|
||||
|
||||
// Session sub-schema
|
||||
const SessionSchema = new Schema(
|
||||
|
|
@ -167,165 +133,4 @@ const userSchema = new Schema<IUser>(
|
|||
{ timestamps: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* Search for a single user based on partial data and return matching user document as plain object.
|
||||
* @param {Partial<MongoUser>} searchCriteria - The partial data to use for searching the user.
|
||||
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
|
||||
* @returns {Promise<MongoUser>} A plain object representing the user document, or `null` if no user is found.
|
||||
*/
|
||||
userSchema.statics.findUser = async function (
|
||||
searchCriteria: Partial<IUser>,
|
||||
fieldsToSelect: string | string[] | null = null,
|
||||
) {
|
||||
const query = this.findOne(searchCriteria);
|
||||
if (fieldsToSelect) {
|
||||
query.select(fieldsToSelect);
|
||||
}
|
||||
return await query.lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Count the number of user documents in the collection based on the provided filter.
|
||||
*
|
||||
* @param {Object} [filter={}] - The filter to apply when counting the documents.
|
||||
* @returns {Promise<number>} The count of documents that match the filter.
|
||||
*/
|
||||
userSchema.statics.countUsers = async function (filter: Record<string, any> = {}) {
|
||||
return await this.countDocuments(filter);
|
||||
};
|
||||
/**
|
||||
* Creates a new user, optionally with a TTL of 1 week.
|
||||
* @param {MongoUser} data - The user data to be created, must contain user_id.
|
||||
* @param {boolean} [disableTTL=true] - Whether to disable the TTL. Defaults to `true`.
|
||||
* @param {boolean} [returnUser=false] - Whether to return the created user object.
|
||||
* @returns {Promise<ObjectId|MongoUser>} A promise that resolves to the created user document ID or user object.
|
||||
* @throws {Error} If a user with the same user_id already exists.
|
||||
*/
|
||||
userSchema.statics.createUser = async function (
|
||||
data: Partial<IUser>,
|
||||
balanceConfig: any,
|
||||
disableTTL: boolean = true,
|
||||
returnUser: boolean = false,
|
||||
) {
|
||||
const userData: Partial<IUser> = {
|
||||
...data,
|
||||
expiresAt: disableTTL ? null : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds
|
||||
};
|
||||
|
||||
if (disableTTL) {
|
||||
delete userData.expiresAt;
|
||||
}
|
||||
|
||||
const user = await this.create(userData);
|
||||
|
||||
// If balance is enabled, create or update a balance record for the user using global.interfaceConfig.balance
|
||||
if (balanceConfig?.enabled && balanceConfig?.startBalance) {
|
||||
const update = {
|
||||
$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,
|
||||
};
|
||||
}
|
||||
|
||||
const balanceModel = mongoose.model('Balance', balanceSchema);
|
||||
await balanceModel
|
||||
.findOneAndUpdate({ user: user._id }, update, { upsert: true, new: true })
|
||||
.lean();
|
||||
}
|
||||
|
||||
if (returnUser) {
|
||||
return user.toObject();
|
||||
}
|
||||
return user._id;
|
||||
};
|
||||
/**
|
||||
* Update a user with new data without overwriting existing properties.
|
||||
*
|
||||
* @param {string} userId - The ID of the user to update.
|
||||
* @param {Object} updateData - An object containing the properties to update.
|
||||
* @returns {Promise<MongoUser>} The updated user document as a plain object, or `null` if no user is found.
|
||||
*/
|
||||
userSchema.statics.updateUser = async function (userId: string, updateData: Partial<IUser>) {
|
||||
const updateOperation = {
|
||||
$set: updateData,
|
||||
$unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL
|
||||
};
|
||||
return await this.findByIdAndUpdate(userId, updateOperation, {
|
||||
new: true,
|
||||
runValidators: true,
|
||||
}).lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve a user by ID and convert the found user document to a plain object.
|
||||
*
|
||||
* @param {string} userId - The ID of the user to find and return as a plain object.
|
||||
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
|
||||
* @returns {Promise<MongoUser>} A plain object representing the user document, or `null` if no user is found.
|
||||
*/
|
||||
userSchema.statics.getUserById = async function (
|
||||
userId: string,
|
||||
fieldsToSelect: string | string[] | null = null,
|
||||
) {
|
||||
const query = this.findById(userId);
|
||||
if (fieldsToSelect) {
|
||||
query.select(fieldsToSelect);
|
||||
}
|
||||
return await query.lean();
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a user by their unique ID.
|
||||
*
|
||||
* @param {string} userId - The ID of the user to delete.
|
||||
* @returns {Promise<{ deletedCount: number }>} An object indicating the number of deleted documents.
|
||||
*/
|
||||
userSchema.statics.deleteUserById = async function (userId: string) {
|
||||
try {
|
||||
const result = await this.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: any) {
|
||||
throw new Error('Error deleting user: ' + error?.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a JWT token for a given user.
|
||||
*
|
||||
* @param {MongoUser} user - The user for whom the token is being generated.
|
||||
* @returns {Promise<string>} A promise that resolves to a JWT token.
|
||||
*/
|
||||
userSchema.statics.generateToken = async function (user: IUser): Promise<string> {
|
||||
if (!user) {
|
||||
throw new Error('No user provided');
|
||||
}
|
||||
|
||||
const expires = eval(process.env.SESSION_EXPIRY ?? '0') ?? 1000 * 60 * 15;
|
||||
|
||||
return await signPayload({
|
||||
payload: {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
provider: user.provider,
|
||||
email: user.email,
|
||||
},
|
||||
secret: process.env.JWT_SECRET,
|
||||
expirationTime: expires / 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export default userSchema;
|
||||
|
|
|
|||
223
packages/data-schemas/src/types/README.md
Normal file
223
packages/data-schemas/src/types/README.md
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# Data Schemas - Refactored Architecture
|
||||
|
||||
This package has been refactored to follow a clean, modular architecture with clear separation of concerns.
|
||||
|
||||
## 📁 Directory Structure
|
||||
|
||||
```
|
||||
packages/data-schemas/src/
|
||||
├── index.ts # Main exports
|
||||
├── schema/ # 🗄️ Mongoose schema definitions
|
||||
│ ├── user.ts
|
||||
│ ├── session.ts
|
||||
│ └── token.ts
|
||||
├── models/ # 🏗️ Mongoose model instances
|
||||
│ └── index.ts
|
||||
├── methods/ # ⚙️ Business logic functions
|
||||
│ ├── index.ts
|
||||
│ ├── user.ts
|
||||
│ ├── session.ts
|
||||
│ └── token.ts
|
||||
└── types/ # 📋 TypeScript interfaces & types
|
||||
├── index.ts
|
||||
├── user.ts
|
||||
├── session.ts
|
||||
└── token.ts
|
||||
```
|
||||
|
||||
## 🎯 Key Benefits
|
||||
|
||||
### 1. **Separation of Concerns**
|
||||
- **Schema**: Pure Mongoose schema definitions
|
||||
- **Models**: Model instances created once
|
||||
- **Methods**: Business logic as pure functions
|
||||
- **Types**: Shared TypeScript interfaces
|
||||
|
||||
### 2. **No Dynamic Imports**
|
||||
- Models are created once in the models directory
|
||||
- Methods import model instances directly
|
||||
- No more dynamic `import()` calls
|
||||
|
||||
### 3. **Better Type Safety**
|
||||
- Shared types across all layers
|
||||
- Proper TypeScript typing throughout
|
||||
- Clear interfaces for all operations
|
||||
|
||||
### 4. **Pure Functions**
|
||||
- Methods are now side-effect free
|
||||
- Easy to test and reason about
|
||||
- No magic or hidden dependencies
|
||||
|
||||
## 🚀 Migration Guide
|
||||
|
||||
### Before (Static Methods)
|
||||
```typescript
|
||||
import { User } from 'some-model-registry';
|
||||
|
||||
// Old way with static methods
|
||||
const user = await User.findUser({ email: 'test@example.com' });
|
||||
const result = await User.deleteUserById(userId);
|
||||
```
|
||||
|
||||
### After (Pure Functions)
|
||||
```typescript
|
||||
import { findUser, deleteUserById } from '~/methods';
|
||||
|
||||
// New way with pure functions
|
||||
const user = await findUser({ email: 'test@example.com' });
|
||||
const result = await deleteUserById(userId);
|
||||
```
|
||||
|
||||
## 📚 Usage Examples
|
||||
|
||||
### User Operations
|
||||
```typescript
|
||||
import {
|
||||
findUser,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUserById,
|
||||
generateToken
|
||||
} from '~/methods';
|
||||
|
||||
// Find a user
|
||||
const user = await findUser(
|
||||
{ email: 'user@example.com' },
|
||||
'name email role'
|
||||
);
|
||||
|
||||
// Create a user with balance config
|
||||
const newUser = await createUser(
|
||||
{ email: 'new@example.com', name: 'John' },
|
||||
{ enabled: true, startBalance: 100 },
|
||||
true, // disable TTL
|
||||
true // return user object
|
||||
);
|
||||
|
||||
// Update user
|
||||
const updated = await updateUser(userId, { name: 'Jane' });
|
||||
|
||||
// Delete user
|
||||
const result = await deleteUserById(userId);
|
||||
|
||||
// Generate JWT token
|
||||
const token = await generateToken(user);
|
||||
```
|
||||
|
||||
### Session Operations
|
||||
```typescript
|
||||
import {
|
||||
createSession,
|
||||
findSession,
|
||||
deleteSession,
|
||||
deleteAllUserSessions
|
||||
} from '~/methods';
|
||||
|
||||
// Create session
|
||||
const { session, refreshToken } = await createSession(userId);
|
||||
|
||||
// Find session by refresh token
|
||||
const foundSession = await findSession({ refreshToken });
|
||||
|
||||
// Delete specific session
|
||||
await deleteSession({ sessionId });
|
||||
|
||||
// Delete all user sessions
|
||||
await deleteAllUserSessions(userId, {
|
||||
excludeCurrentSession: true,
|
||||
currentSessionId
|
||||
});
|
||||
```
|
||||
|
||||
### Token Operations
|
||||
```typescript
|
||||
import {
|
||||
createToken,
|
||||
findToken,
|
||||
updateToken,
|
||||
deleteTokens
|
||||
} from '~/methods';
|
||||
|
||||
// Create token
|
||||
const token = await createToken({
|
||||
userId,
|
||||
token: 'abc123',
|
||||
type: 'verification',
|
||||
expiresIn: 3600 // 1 hour
|
||||
});
|
||||
|
||||
// Find token
|
||||
const foundToken = await findToken({
|
||||
token: 'abc123',
|
||||
type: 'verification'
|
||||
});
|
||||
|
||||
// Update token
|
||||
const updated = await updateToken(
|
||||
{ token: 'abc123' },
|
||||
{ type: 'password-reset' }
|
||||
);
|
||||
|
||||
// Delete tokens
|
||||
await deleteTokens({ userId });
|
||||
```
|
||||
|
||||
## 🔧 Path Aliases
|
||||
|
||||
The project uses `~/` as an alias for `./src/`:
|
||||
|
||||
```typescript
|
||||
import { IUser } from '~/types';
|
||||
import { User } from '~/models';
|
||||
import { findUser } from '~/methods';
|
||||
import userSchema from '~/schema/user';
|
||||
```
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
|
||||
1. **Static Methods Removed**: All static methods have been removed from schema files
|
||||
2. **Function Signatures**: Methods no longer take model as first parameter
|
||||
3. **Import Paths**: Import from `~/methods` instead of calling static methods
|
||||
4. **Type Definitions**: Types moved to dedicated `~/types` directory
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
The new pure function approach makes testing much easier:
|
||||
|
||||
```typescript
|
||||
import { findUser } from '~/methods';
|
||||
|
||||
// Easy to mock and test
|
||||
jest.mock('~/models', () => ({
|
||||
User: {
|
||||
findOne: jest.fn().mockReturnValue({
|
||||
select: jest.fn().mockReturnValue({
|
||||
lean: jest.fn().mockResolvedValue(mockUser)
|
||||
})
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
test('findUser should return user', async () => {
|
||||
const result = await findUser({ email: 'test@example.com' });
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
```
|
||||
|
||||
## 🔄 Error Handling
|
||||
|
||||
All methods include proper error handling with typed errors:
|
||||
|
||||
```typescript
|
||||
import { SessionError } from '~/types';
|
||||
|
||||
try {
|
||||
const session = await createSession(userId);
|
||||
} catch (error) {
|
||||
if (error instanceof SessionError) {
|
||||
console.log('Session error:', error.code, error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This refactoring provides a much cleaner, more maintainable, and type-safe architecture for data operations.
|
||||
8
packages/data-schemas/src/types/index.ts
Normal file
8
packages/data-schemas/src/types/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// User types
|
||||
export * from './user';
|
||||
|
||||
// Session types
|
||||
export * from './session';
|
||||
|
||||
// Token types
|
||||
export * from './token';
|
||||
52
packages/data-schemas/src/types/session.ts
Normal file
52
packages/data-schemas/src/types/session.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Document, Types } from 'mongoose';
|
||||
|
||||
export interface ISession extends Document {
|
||||
refreshTokenHash: string;
|
||||
expiration: Date;
|
||||
user: Types.ObjectId;
|
||||
}
|
||||
|
||||
export interface CreateSessionOptions {
|
||||
expiration?: Date;
|
||||
}
|
||||
|
||||
export interface SessionSearchParams {
|
||||
refreshToken?: string;
|
||||
userId?: string;
|
||||
sessionId?: string | { sessionId: string };
|
||||
}
|
||||
|
||||
export interface SessionQueryOptions {
|
||||
lean?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteSessionParams {
|
||||
refreshToken?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface DeleteAllSessionsOptions {
|
||||
excludeCurrentSession?: boolean;
|
||||
currentSessionId?: string;
|
||||
}
|
||||
|
||||
export interface SessionResult {
|
||||
session: Partial<ISession>;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface SignPayloadParams {
|
||||
payload: Record<string, unknown>;
|
||||
secret?: string;
|
||||
expirationTime: number;
|
||||
}
|
||||
|
||||
export class SessionError extends Error {
|
||||
public code: string;
|
||||
|
||||
constructor(message: string, code: string = 'SESSION_ERROR') {
|
||||
super(message);
|
||||
this.name = 'SessionError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
42
packages/data-schemas/src/types/token.ts
Normal file
42
packages/data-schemas/src/types/token.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Document, Types } from 'mongoose';
|
||||
|
||||
export interface IToken extends Document {
|
||||
userId: Types.ObjectId;
|
||||
email?: string;
|
||||
type?: string;
|
||||
identifier?: string;
|
||||
token: string;
|
||||
createdAt: Date;
|
||||
expiresAt: Date;
|
||||
metadata?: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TokenCreateData {
|
||||
userId: Types.ObjectId | string;
|
||||
email?: string;
|
||||
type?: string;
|
||||
identifier?: string;
|
||||
token: string;
|
||||
expiresIn: number;
|
||||
metadata?: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TokenQuery {
|
||||
userId?: Types.ObjectId | string;
|
||||
token?: string;
|
||||
email?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
export interface TokenUpdateData {
|
||||
email?: string;
|
||||
type?: string;
|
||||
identifier?: string;
|
||||
token?: string;
|
||||
expiresAt?: Date;
|
||||
metadata?: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TokenDeleteResult {
|
||||
deletedCount?: number;
|
||||
}
|
||||
72
packages/data-schemas/src/types/user.ts
Normal file
72
packages/data-schemas/src/types/user.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Document, Types } from 'mongoose';
|
||||
|
||||
export interface IUser extends Document {
|
||||
name?: string;
|
||||
username?: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
password?: string;
|
||||
avatar?: string;
|
||||
provider: string;
|
||||
role?: string;
|
||||
googleId?: string;
|
||||
facebookId?: string;
|
||||
openidId?: string;
|
||||
samlId?: string;
|
||||
ldapId?: string;
|
||||
githubId?: string;
|
||||
discordId?: string;
|
||||
appleId?: string;
|
||||
plugins?: unknown[];
|
||||
twoFactorEnabled?: boolean;
|
||||
totpSecret?: string;
|
||||
backupCodes?: Array<{
|
||||
codeHash: string;
|
||||
used: boolean;
|
||||
usedAt?: Date | null;
|
||||
}>;
|
||||
refreshToken?: Array<{
|
||||
refreshToken: string;
|
||||
}>;
|
||||
expiresAt?: Date;
|
||||
termsAccepted?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
export interface BalanceConfig {
|
||||
enabled?: boolean;
|
||||
startBalance?: number;
|
||||
autoRefillEnabled?: boolean;
|
||||
refillIntervalValue?: number;
|
||||
refillIntervalUnit?: string;
|
||||
refillAmount?: number;
|
||||
}
|
||||
|
||||
export interface UserCreateData extends Partial<IUser> {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface UserUpdateResult {
|
||||
deletedCount: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UserSearchCriteria {
|
||||
email?: string;
|
||||
username?: string;
|
||||
googleId?: string;
|
||||
facebookId?: string;
|
||||
openidId?: string;
|
||||
samlId?: string;
|
||||
ldapId?: string;
|
||||
githubId?: string;
|
||||
discordId?: string;
|
||||
appleId?: string;
|
||||
_id?: Types.ObjectId | string;
|
||||
}
|
||||
|
||||
export interface UserQueryOptions {
|
||||
fieldsToSelect?: string | string[] | null;
|
||||
lean?: boolean;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue