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 bcrypt = require('bcryptjs');
|
||||||
const { webcrypto } = require('node:crypto');
|
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 { SystemRoles, errorsToString } = require('librechat-data-provider');
|
||||||
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
||||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||||
|
const { getBalanceConfig } = require('~/server/services/Config');
|
||||||
const { registerSchema } = require('~/strategies/validators');
|
const { registerSchema } = require('~/strategies/validators');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
const db = require('~/lib/db/connectDb');
|
|
||||||
const { getBalanceConfig } = require('~/server/services/Config');
|
|
||||||
|
|
||||||
const domains = {
|
const domains = {
|
||||||
client: process.env.DOMAIN_CLIENT,
|
client: process.env.DOMAIN_CLIENT,
|
||||||
|
|
@ -24,14 +39,13 @@ const genericVerificationMessage = 'Please check your email to verify your email
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const logoutUser = async (req, refreshToken) => {
|
const logoutUser = async (req, refreshToken) => {
|
||||||
const { Session } = db.models;
|
|
||||||
try {
|
try {
|
||||||
const userId = req.user._id;
|
const userId = req.user._id;
|
||||||
const session = await Session.findSession({ userId: userId, refreshToken });
|
const session = await findSession({ userId: userId, refreshToken });
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
try {
|
try {
|
||||||
await Session.deleteSession({ sessionId: session._id });
|
await deleteSession({ sessionId: session._id });
|
||||||
} catch (deleteErr) {
|
} catch (deleteErr) {
|
||||||
logger.error('[logoutUser] Failed to delete session.', deleteErr);
|
logger.error('[logoutUser] Failed to delete session.', deleteErr);
|
||||||
return { status: 500, message: 'Failed to delete session.' };
|
return { status: 500, message: 'Failed to delete session.' };
|
||||||
|
|
@ -83,7 +97,7 @@ const sendVerificationEmail = async (user) => {
|
||||||
template: 'verifyEmail.handlebars',
|
template: 'verifyEmail.handlebars',
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.models.Token.createToken({
|
await createToken({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
token: hash,
|
token: hash,
|
||||||
|
|
@ -102,8 +116,7 @@ const verifyEmail = async (req) => {
|
||||||
const { email, token } = req.body;
|
const { email, token } = req.body;
|
||||||
const decodedEmail = decodeURIComponent(email);
|
const decodedEmail = decodeURIComponent(email);
|
||||||
|
|
||||||
const { User, Token } = db.models;
|
const user = await findUser({ email: decodedEmail }, 'email _id emailVerified');
|
||||||
const user = await User.findUser({ email: decodedEmail }, 'email _id emailVerified');
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(`[verifyEmail] [User not found] [Email: ${decodedEmail}]`);
|
logger.warn(`[verifyEmail] [User not found] [Email: ${decodedEmail}]`);
|
||||||
|
|
@ -115,7 +128,7 @@ const verifyEmail = async (req) => {
|
||||||
return { message: 'Email already verified', status: 'success' };
|
return { message: 'Email already verified', status: 'success' };
|
||||||
}
|
}
|
||||||
|
|
||||||
let emailVerificationData = await Token.findToken({ email: decodedEmail });
|
let emailVerificationData = await findToken({ email: decodedEmail });
|
||||||
|
|
||||||
if (!emailVerificationData) {
|
if (!emailVerificationData) {
|
||||||
logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`);
|
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');
|
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) {
|
if (!updatedUser) {
|
||||||
logger.warn(`[verifyEmail] [User update failed] [Email: ${decodedEmail}]`);
|
logger.warn(`[verifyEmail] [User update failed] [Email: ${decodedEmail}]`);
|
||||||
return new Error('Failed to update user verification status');
|
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}]`);
|
logger.info(`[verifyEmail] Email verification successful [Email: ${decodedEmail}]`);
|
||||||
return { message: 'Email verification was successful', status: 'success' };
|
return { message: 'Email verification was successful', status: 'success' };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user.
|
* Register a new user.
|
||||||
* @param {MongoUser} user <email, password, name, username>
|
* @param {MongoUser} user <email, password, name, username>
|
||||||
|
|
@ -148,7 +163,6 @@ const verifyEmail = async (req) => {
|
||||||
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
|
||||||
*/
|
*/
|
||||||
const registerUser = async (user, additionalData = {}) => {
|
const registerUser = async (user, additionalData = {}) => {
|
||||||
const { User } = db.models;
|
|
||||||
const { error } = registerSchema.safeParse(user);
|
const { error } = registerSchema.safeParse(user);
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorMessage = errorsToString(error.errors);
|
const errorMessage = errorsToString(error.errors);
|
||||||
|
|
@ -165,7 +179,7 @@ const registerUser = async (user, additionalData = {}) => {
|
||||||
|
|
||||||
let newUserId;
|
let newUserId;
|
||||||
try {
|
try {
|
||||||
const existingUser = await User.findUser({ email }, 'email _id');
|
const existingUser = await findUser({ email }, 'email _id');
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -187,7 +201,7 @@ const registerUser = async (user, additionalData = {}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
//determine if this is the first registered user (not counting anonymous_user)
|
//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 salt = bcrypt.genSaltSync(10);
|
||||||
const newUserData = {
|
const newUserData = {
|
||||||
|
|
@ -204,8 +218,8 @@ const registerUser = async (user, additionalData = {}) => {
|
||||||
const emailEnabled = checkEmailConfig();
|
const emailEnabled = checkEmailConfig();
|
||||||
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN);
|
||||||
const balanceConfig = await getBalanceConfig();
|
const balanceConfig = await getBalanceConfig();
|
||||||
|
|
||||||
const newUser = await User.createUser(newUserData, balanceConfig, disableTTL, true);
|
const newUser = await createUser(newUserData, balanceConfig, disableTTL, true);
|
||||||
newUserId = newUser._id;
|
newUserId = newUser._id;
|
||||||
if (emailEnabled && !newUser.emailVerified) {
|
if (emailEnabled && !newUser.emailVerified) {
|
||||||
await sendVerificationEmail({
|
await sendVerificationEmail({
|
||||||
|
|
@ -214,14 +228,14 @@ const registerUser = async (user, additionalData = {}) => {
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await User.updateUser(newUserId, { emailVerified: true });
|
await updateUser(newUserId, { emailVerified: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: 200, message: genericVerificationMessage };
|
return { status: 200, message: genericVerificationMessage };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[registerUser] Error in registering user:', err);
|
logger.error('[registerUser] Error in registering user:', err);
|
||||||
if (newUserId) {
|
if (newUserId) {
|
||||||
const result = await User.deleteUserById(newUserId);
|
const result = await deleteUserById(newUserId);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
|
`[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`,
|
||||||
);
|
);
|
||||||
|
|
@ -236,8 +250,7 @@ const registerUser = async (user, additionalData = {}) => {
|
||||||
*/
|
*/
|
||||||
const requestPasswordReset = async (req) => {
|
const requestPasswordReset = async (req) => {
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
const { User, Token } = db.models;
|
const user = await findUser({ email }, 'email _id');
|
||||||
const user = await User.findUser({ email }, 'email _id');
|
|
||||||
const emailEnabled = checkEmailConfig();
|
const emailEnabled = checkEmailConfig();
|
||||||
|
|
||||||
logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`);
|
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();
|
const [resetToken, hash] = createTokenHash();
|
||||||
|
|
||||||
await Token.createToken({
|
await createToken({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
token: hash,
|
token: hash,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
|
|
@ -298,8 +311,7 @@ const requestPasswordReset = async (req) => {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const resetPassword = async (userId, token, password) => {
|
const resetPassword = async (userId, token, password) => {
|
||||||
const { User, Token } = db.models;
|
let passwordResetToken = await findToken({
|
||||||
let passwordResetToken = await Token.findToken({
|
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -314,7 +326,7 @@ const resetPassword = async (userId, token, password) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = bcrypt.hashSync(password, 10);
|
const hash = bcrypt.hashSync(password, 10);
|
||||||
const user = await User.updateUser(userId, { password: hash });
|
const user = await updateUser(userId, { password: hash });
|
||||||
|
|
||||||
if (checkEmailConfig()) {
|
if (checkEmailConfig()) {
|
||||||
await sendEmail({
|
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}]`);
|
logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`);
|
||||||
return { message: 'Password reset was successful' };
|
return { message: 'Password reset was successful' };
|
||||||
};
|
};
|
||||||
|
|
@ -344,20 +356,19 @@ const resetPassword = async (userId, token, password) => {
|
||||||
*/
|
*/
|
||||||
const setAuthTokens = async (userId, res, sessionId = null) => {
|
const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||||
try {
|
try {
|
||||||
const { User, Session } = db.models;
|
const user = await getUserById(userId);
|
||||||
const user = await User.getUserById(userId);
|
const token = await generateToken(user);
|
||||||
const token = await User.generateToken(user);
|
|
||||||
|
|
||||||
let session;
|
let session;
|
||||||
let refreshToken;
|
let refreshToken;
|
||||||
let refreshTokenExpires;
|
let refreshTokenExpires;
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
session = await Session.findSession({ sessionId: sessionId }, { lean: false });
|
session = await findSession({ sessionId: sessionId }, { lean: false });
|
||||||
refreshTokenExpires = session.expiration.getTime();
|
refreshTokenExpires = session.expiration.getTime();
|
||||||
refreshToken = await Session.generateRefreshToken(session);
|
refreshToken = await generateRefreshToken(session);
|
||||||
} else {
|
} else {
|
||||||
const result = await Session.createSession(userId);
|
const result = await createSession(userId);
|
||||||
session = result.session;
|
session = result.session;
|
||||||
refreshToken = result.refreshToken;
|
refreshToken = result.refreshToken;
|
||||||
refreshTokenExpires = session.expiration.getTime();
|
refreshTokenExpires = session.expiration.getTime();
|
||||||
|
|
@ -381,6 +392,7 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function setOpenIDAuthTokens
|
* @function setOpenIDAuthTokens
|
||||||
* Set OpenID Authentication Tokens
|
* Set OpenID Authentication Tokens
|
||||||
|
|
@ -436,9 +448,8 @@ const setOpenIDAuthTokens = (tokenset, res) => {
|
||||||
const resendVerificationEmail = async (req) => {
|
const resendVerificationEmail = async (req) => {
|
||||||
try {
|
try {
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
const { User, Token } = db.models;
|
await deleteTokens(email);
|
||||||
await Token.deleteTokens(email);
|
const user = await findUser({ email }, 'email _id name');
|
||||||
const user = await User.findUser({ email }, 'email _id name');
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`);
|
||||||
|
|
@ -463,7 +474,7 @@ const resendVerificationEmail = async (req) => {
|
||||||
template: 'verifyEmail.handlebars',
|
template: 'verifyEmail.handlebars',
|
||||||
});
|
});
|
||||||
|
|
||||||
await Token.createToken({
|
await createToken({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
token: hash,
|
token: hash,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { connectDb, getModels } from '@librechat/backend/lib/db/connectDb';
|
import { connectDb, getModels } from '@librechat/backend/lib/db/connectDb';
|
||||||
import { deleteMessages, deleteConvos, User, Balance } from '@librechat/backend/models';
|
import { findUser, deleteAllUserSessions } from '@librechat/data-schemas';
|
||||||
import { Transaction } from '@librechat/backend/models/Transaction';
|
import { deleteMessages, deleteConvos } from '@librechat/backend/models';
|
||||||
|
|
||||||
type TUser = { email: string; password: string };
|
type TUser = { email: string; password: string };
|
||||||
|
|
||||||
export default async function cleanupUser(user: TUser) {
|
export default async function cleanupUser(user: TUser) {
|
||||||
|
|
@ -10,29 +11,38 @@ export default async function cleanupUser(user: TUser) {
|
||||||
const db = await connectDb();
|
const db = await connectDb();
|
||||||
console.log('🤖: ✅ Connected to Database');
|
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');
|
console.log('🤖: ✅ Found user in Database');
|
||||||
|
|
||||||
// Delete all conversations & associated messages
|
// Delete all conversations & associated messages
|
||||||
const { deletedCount, messages } = await deleteConvos(user, {});
|
const { deletedCount, messages } = await deleteConvos(userId, {});
|
||||||
|
|
||||||
if (messages.deletedCount > 0 || deletedCount > 0) {
|
if (messages.deletedCount > 0 || deletedCount > 0) {
|
||||||
console.log(`🤖: ✅ Deleted ${deletedCount} convos & ${messages.deletedCount} messages`);
|
console.log(`🤖: ✅ Deleted ${deletedCount} convos & ${messages.deletedCount} messages`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all user messages are deleted
|
// Ensure all user messages are deleted
|
||||||
const { deletedCount: deletedMessages } = await deleteMessages({ user });
|
const { deletedCount: deletedMessages } = await deleteMessages({ user: userId });
|
||||||
if (deletedMessages > 0) {
|
if (deletedMessages > 0) {
|
||||||
console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`);
|
console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix this to delete all user sessions with the user's email
|
// Delete all user sessions
|
||||||
const { User, Session } = getModels();
|
await deleteAllUserSessions(userId.toString());
|
||||||
await Session.deleteAllUserSessions(user);
|
|
||||||
|
|
||||||
await User.deleteMany({ _id: user });
|
// Get models from the registered models
|
||||||
await Balance.deleteMany({ user });
|
const { User, Balance, Transaction } = getModels();
|
||||||
await Transaction.deleteMany({ user });
|
|
||||||
|
// 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');
|
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 { default as actionSchema } from './schema/action';
|
||||||
export type { IAction } 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 { default as roleSchema } from './schema/role';
|
||||||
export type { IRole } 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 { default as shareSchema } from './schema/share';
|
||||||
export type { ISharedLink } 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 { default as toolCallSchema } from './schema/toolCall';
|
||||||
export type { IToolCallData } from './schema/toolCall';
|
export type { IToolCallData } from './schema/toolCall';
|
||||||
|
|
||||||
export { default as transactionSchema } from './schema/transaction';
|
export { default as transactionSchema } from './schema/transaction';
|
||||||
export type { ITransaction } 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 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';
|
|
||||||
|
|
||||||
export const registerModels = (mongoose: Mongoose) => {
|
// Import schemas
|
||||||
const User = registerUserModel(mongoose);
|
import userSchema from '~/schema/user';
|
||||||
const Session = registerSessionModel(mongoose);
|
import sessionSchema from '~/schema/session';
|
||||||
const Token = registerTokenModel(mongoose);
|
import tokenSchema from '~/schema/token';
|
||||||
const Message = registerMessageModel(mongoose);
|
import balanceSchema from '~/schema/balance';
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
// Import types
|
||||||
User,
|
import { IUser, ISession, IToken } from '~/types';
|
||||||
Session,
|
import { IBalance } from '~/schema/balance';
|
||||||
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) => {
|
// Create and export model instances
|
||||||
return mongoose.models.Session || mongoose.model('Session', sessionSchema);
|
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);
|
||||||
|
|
||||||
const registerUserModel = (mongoose: Mongoose) => {
|
// Default export with all models
|
||||||
return mongoose.models.User || mongoose.model('User', userSchema);
|
export default {
|
||||||
};
|
User,
|
||||||
|
Session,
|
||||||
const registerTokenModel = (mongoose: Mongoose) => {
|
Token,
|
||||||
return mongoose.models.Token || mongoose.model('Token', tokenSchema);
|
Balance,
|
||||||
};
|
|
||||||
|
|
||||||
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 jwt from 'jsonwebtoken';
|
||||||
import logger from '../config/winston';
|
import { webcrypto } from 'node:crypto';
|
||||||
const { webcrypto } = require('node:crypto');
|
import { ISession, SignPayloadParams } from '~/types';
|
||||||
|
|
||||||
export interface ISession extends Document {
|
|
||||||
refreshTokenHash: string;
|
|
||||||
expiration: Date;
|
|
||||||
user: Types.ObjectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionSchema: Schema<ISession> = new Schema({
|
const sessionSchema: Schema<ISession> = new Schema({
|
||||||
refreshTokenHash: {
|
refreshTokenHash: {
|
||||||
|
|
@ -26,224 +20,18 @@ const sessionSchema: Schema<ISession> = new Schema({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
export async function signPayload({
|
||||||
* Error class for Session-related errors
|
payload,
|
||||||
*/
|
secret,
|
||||||
class SessionError extends Error {
|
expirationTime,
|
||||||
constructor(message, code = 'SESSION_ERROR') {
|
}: SignPayloadParams): Promise<string> {
|
||||||
super(message);
|
return jwt.sign(payload, secret!, { expiresIn: expirationTime });
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 data = new TextEncoder().encode(str);
|
||||||
const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
|
const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
|
||||||
return Buffer.from(hashBuffer).toString('hex');
|
return Buffer.from(hashBuffer).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default sessionSchema;
|
export default sessionSchema;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,5 @@
|
||||||
import { Schema, Document, Types } from 'mongoose';
|
import { Schema } from 'mongoose';
|
||||||
import { logger } from '~/config';
|
import { IToken } from '~/types';
|
||||||
|
|
||||||
export interface IToken extends Document {
|
|
||||||
userId: Types.ObjectId;
|
|
||||||
email?: string;
|
|
||||||
type?: string;
|
|
||||||
identifier?: string;
|
|
||||||
token: string;
|
|
||||||
createdAt: Date;
|
|
||||||
expiresAt: Date;
|
|
||||||
metadata?: Map<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenSchema: Schema<IToken> = new Schema({
|
const tokenSchema: Schema<IToken> = new Schema({
|
||||||
userId: {
|
userId: {
|
||||||
|
|
@ -48,116 +37,4 @@ const tokenSchema: Schema<IToken> = new Schema({
|
||||||
|
|
||||||
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
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;
|
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 { SystemRoles } from 'librechat-data-provider';
|
||||||
import { default as balanceSchema } from './balance';
|
import { IUser } from '~/types';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session sub-schema
|
// Session sub-schema
|
||||||
const SessionSchema = new Schema(
|
const SessionSchema = new Schema(
|
||||||
|
|
@ -167,165 +133,4 @@ const userSchema = new Schema<IUser>(
|
||||||
{ timestamps: true },
|
{ 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;
|
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