feat: add terms acceptance timestamp tracking and migration script

This commit is contained in:
Marco Beretta 2025-11-30 23:53:23 +01:00
parent 3e8ef24cfa
commit 44f7aad007
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
10 changed files with 134 additions and 4 deletions

View file

@ -77,7 +77,10 @@ const getTermsStatusController = async (req, res) => {
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.status(200).json({ termsAccepted: !!user.termsAccepted });
res.status(200).json({
termsAccepted: !!user.termsAccepted,
termsAcceptedAt: user.termsAcceptedAt || null,
});
} catch (error) {
logger.error('Error fetching terms acceptance status:', error);
res.status(500).json({ message: 'Error fetching terms acceptance status' });
@ -86,7 +89,14 @@ const getTermsStatusController = async (req, res) => {
const acceptTermsController = async (req, res) => {
try {
const user = await User.findByIdAndUpdate(req.user.id, { termsAccepted: true }, { new: true });
const user = await User.findByIdAndUpdate(
req.user.id,
{
termsAccepted: true,
termsAcceptedAt: new Date(),
},
{ new: true },
);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}

View file

@ -1068,6 +1068,7 @@ export const useAcceptTermsMutation = (
onSuccess: (data, variables, context) => {
queryClient.setQueryData<t.TUserTermsResponse>([QueryKeys.userTerms], {
termsAccepted: true,
termsAcceptedAt: new Date().toISOString(),
});
options?.onSuccess?.(data, variables, context);
},

View file

@ -7,7 +7,10 @@ const connect = require('./connect');
const listUsers = async () => {
try {
await connect();
const users = await User.find({}, 'email provider avatar username name createdAt');
const users = await User.find(
{},
'email provider avatar username name createdAt termsAccepted termsAcceptedAt',
);
console.log('\nUser List:');
console.log('----------------------------------------');
@ -18,6 +21,10 @@ const listUsers = async () => {
console.log(`Name: ${user.name || 'N/A'}`);
console.log(`Provider: ${user.provider || 'email'}`);
console.log(`Created: ${user.createdAt}`);
console.log(`Terms Accepted: ${user.termsAccepted ? 'Yes' : 'No'}`);
console.log(
`Terms Accepted At: ${user.termsAcceptedAt ? user.termsAcceptedAt.toISOString() : 'N/A'}`,
);
console.log('----------------------------------------');
});

View file

@ -0,0 +1,100 @@
const path = require('path');
const mongoose = require('mongoose');
const { User } = require('@librechat/data-schemas').createModels(mongoose);
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { askQuestion, silentExit } = require('./helpers');
const connect = require('./connect');
/**
* Migration script for Terms Acceptance Timestamp Tracking
*
* This script migrates existing users who have termsAccepted: true but no termsAcceptedAt timestamp.
* For these users, it sets termsAcceptedAt to their account creation date (createdAt) as a fallback.
*
* Usage: npm run migrate:terms-timestamp
*/
(async () => {
await connect();
console.purple('--------------------------');
console.purple('Migrate Terms Acceptance Timestamps');
console.purple('--------------------------');
// Count users that need migration
const usersToMigrate = await User.countDocuments({
termsAccepted: true,
$or: [{ termsAcceptedAt: null }, { termsAcceptedAt: { $exists: false } }],
});
if (usersToMigrate === 0) {
console.green(
'No users need migration. All users with termsAccepted: true already have a termsAcceptedAt timestamp.',
);
silentExit(0);
}
console.yellow(
`Found ${usersToMigrate} user(s) with termsAccepted: true but no termsAcceptedAt timestamp.`,
);
console.yellow(
'These users will have their termsAcceptedAt set to their account creation date (createdAt).',
);
const confirm = await askQuestion('Are you sure you want to proceed? (y/n): ');
if (confirm.toLowerCase() !== 'y') {
console.yellow('Operation cancelled.');
silentExit(0);
}
try {
// Find all users that need migration and update them
const cursor = User.find({
termsAccepted: true,
$or: [{ termsAcceptedAt: null }, { termsAcceptedAt: { $exists: false } }],
}).cursor();
let migratedCount = 0;
let errorCount = 0;
for await (const user of cursor) {
try {
// Use createdAt as fallback for termsAcceptedAt
const termsAcceptedAt = user.createdAt || new Date();
await User.updateOne({ _id: user._id }, { $set: { termsAcceptedAt } });
migratedCount++;
if (migratedCount % 100 === 0) {
console.yellow(`Migrated ${migratedCount} users...`);
}
} catch (error) {
console.red(`Error migrating user ${user._id}: ${error.message}`);
errorCount++;
}
}
console.green(`Migration complete!`);
console.green(`Successfully migrated: ${migratedCount} user(s)`);
if (errorCount > 0) {
console.red(`Errors encountered: ${errorCount}`);
}
} catch (error) {
console.red('Error during migration:', error);
silentExit(1);
}
silentExit(0);
})();
process.on('uncaughtException', (err) => {
if (!err.message.includes('fetch failed')) {
console.error('There was an uncaught error:');
console.error(err);
}
if (err.message.includes('fetch failed')) {
return;
} else {
process.exit(1);
}
});

View file

@ -21,7 +21,10 @@ const connect = require('./connect');
}
try {
const result = await User.updateMany({}, { $set: { termsAccepted: false } });
const result = await User.updateMany(
{},
{ $set: { termsAccepted: false, termsAcceptedAt: null } },
);
console.green(`Updated ${result.modifiedCount} user(s).`);
} catch (error) {
console.red('Error resetting terms acceptance:', error);

View file

@ -81,6 +81,7 @@
"b:balance": "bun config/add-balance.js",
"b:list-balances": "bun config/list-balances.js",
"reset-terms": "node config/reset-terms.js",
"migrate:terms-timestamp": "node config/migrate-terms-timestamp.js",
"flush-cache": "node config/flush-cache.js",
"migrate:agent-permissions:dry-run": "node config/migrate-agent-permissions.js --dry-run",
"migrate:agent-permissions": "node config/migrate-agent-permissions.js",

View file

@ -26,6 +26,7 @@ const ALLOWED_USER_FIELDS = [
'emailVerified',
'twoFactorEnabled',
'termsAccepted',
'termsAcceptedAt',
] as const;
type AllowedUserField = (typeof ALLOWED_USER_FIELDS)[number];

View file

@ -623,6 +623,7 @@ export type TCustomConfigSpeechResponse = { [key: string]: string };
export type TUserTermsResponse = {
termsAccepted: boolean;
termsAcceptedAt: Date | string | null;
};
export type TAcceptTermsResponse = {

View file

@ -132,6 +132,10 @@ const userSchema = new Schema<IUser>(
type: Boolean,
default: false,
},
termsAcceptedAt: {
type: Date,
default: null,
},
personalization: {
type: {
memories: {

View file

@ -31,6 +31,7 @@ export interface IUser extends Document {
}>;
expiresAt?: Date;
termsAccepted?: boolean;
termsAcceptedAt?: Date | null;
personalization?: {
memories?: boolean;
};
@ -68,6 +69,7 @@ export interface UpdateUserRequest {
plugins?: string[];
twoFactorEnabled?: boolean;
termsAccepted?: boolean;
termsAcceptedAt?: Date | null;
personalization?: {
memories?: boolean;
};