From 72c8eb512ffbfb86c3c0ee2e202d9bd37f32e1d1 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 15 Jun 2026 01:52:10 +0200 Subject: [PATCH] feat(user): persist personalization.location with updateUserLocation --- .../src/methods/user.methods.spec.ts | 37 ++++++++++++++++++- packages/data-schemas/src/methods/user.ts | 32 ++++++++++++++++ packages/data-schemas/src/schema/user.ts | 18 +++++++++ packages/data-schemas/src/types/user.ts | 2 + 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/data-schemas/src/methods/user.methods.spec.ts b/packages/data-schemas/src/methods/user.methods.spec.ts index 8869b2006e..439f365add 100644 --- a/packages/data-schemas/src/methods/user.methods.spec.ts +++ b/packages/data-schemas/src/methods/user.methods.spec.ts @@ -1,9 +1,9 @@ import mongoose from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; import type * as t from '~/types'; +import balanceSchema from '~/schema/balance'; import { createUserMethods } from './user'; import userSchema from '~/schema/user'; -import balanceSchema from '~/schema/balance'; /** Mocking crypto for generateToken */ jest.mock('~/crypto', () => ({ @@ -694,6 +694,41 @@ describe('User Methods - Database Tests', () => { }); }); + describe('updateUserLocation', () => { + it('persists a manual location on a user with no personalization object', async () => { + const user = await mongoose.models.User.create({ email: 'loc1@test.com' }); + const updated = await methods.updateUserLocation(user._id.toString(), { + enabled: true, + source: 'manual', + manual: 'Berlin, Germany', + timezone: 'Europe/Berlin', + }); + expect(updated?.personalization?.location?.enabled).toBe(true); + expect(updated?.personalization?.location?.manual).toBe('Berlin, Germany'); + expect(updated?.personalization?.location?.timezone).toBe('Europe/Berlin'); + }); + + it('persists device coordinates and place', async () => { + const user = await mongoose.models.User.create({ email: 'loc2@test.com' }); + const updated = await methods.updateUserLocation(user._id.toString(), { + enabled: true, + source: 'auto', + place: 'Paris, Île-de-France, France', + coordinates: { latitude: 48.85, longitude: 2.35 }, + timezone: 'Europe/Paris', + }); + expect(updated?.personalization?.location?.place).toBe('Paris, Île-de-France, France'); + expect(updated?.personalization?.location?.coordinates?.latitude).toBe(48.85); + }); + + it('returns null for a missing user', async () => { + const result = await methods.updateUserLocation(new mongoose.Types.ObjectId().toString(), { + enabled: false, + }); + expect(result).toBeNull(); + }); + }); + describe('findUsers with options', () => { beforeEach(async () => { await User.create([ diff --git a/packages/data-schemas/src/methods/user.ts b/packages/data-schemas/src/methods/user.ts index 56ba50fae4..d662bb22b2 100644 --- a/packages/data-schemas/src/methods/user.ts +++ b/packages/data-schemas/src/methods/user.ts @@ -106,6 +106,10 @@ export function createUserMethods(mongoose: typeof import('mongoose')): { action: 'install' | 'uninstall', ) => Promise; toggleUserMemories: (userId: string, memoriesEnabled: boolean) => Promise; + updateUserLocation: ( + userId: string, + location: import('librechat-data-provider').TUserLocation, + ) => Promise; } { /** * Normalizes email fields in search criteria to lowercase and trimmed. @@ -338,6 +342,33 @@ export function createUserMethods(mongoose: typeof import('mongoose')): { }).lean(); } + /** + * Update a user's location personalization setting. + * Creates the personalization object if it doesn't exist. + */ + async function updateUserLocation( + userId: string, + location: import('librechat-data-provider').TUserLocation, + ): Promise { + const User = mongoose.models.User; + + const user = await User.findById(userId); + if (!user) { + return null; + } + + const updateOperation = { + $set: { + 'personalization.location': { ...location, updatedAt: new Date() }, + }, + }; + + return await User.findByIdAndUpdate(userId, updateOperation, { + new: true, + runValidators: true, + }).lean(); + } + /** * Search for users by pattern matching on name, email, or username (case-insensitive) * @param searchPattern - The pattern to search for @@ -517,6 +548,7 @@ export function createUserMethods(mongoose: typeof import('mongoose')): { deleteUserById, updateUserPlugins, toggleUserMemories, + updateUserLocation, }; } diff --git a/packages/data-schemas/src/schema/user.ts b/packages/data-schemas/src/schema/user.ts index 8c407e3d36..07f42e7c57 100644 --- a/packages/data-schemas/src/schema/user.ts +++ b/packages/data-schemas/src/schema/user.ts @@ -133,6 +133,24 @@ const userSchema: Schema = new Schema( type: Boolean, default: true, }, + location: { + type: { + enabled: { type: Boolean, default: false }, + source: { type: String, enum: ['auto', 'manual'] }, + manual: { type: String, maxlength: 256 }, + place: { type: String, maxlength: 256 }, + coordinates: { + type: { + latitude: { type: Number }, + longitude: { type: Number }, + }, + default: undefined, + }, + timezone: { type: String, maxlength: 64 }, + updatedAt: { type: Date }, + }, + default: undefined, + }, }, default: {}, }, diff --git a/packages/data-schemas/src/types/user.ts b/packages/data-schemas/src/types/user.ts index b9296a4be5..28062cf167 100644 --- a/packages/data-schemas/src/types/user.ts +++ b/packages/data-schemas/src/types/user.ts @@ -50,6 +50,7 @@ export interface IUser extends Document { termsAccepted?: boolean; personalization?: { memories?: boolean; + location?: import('librechat-data-provider').TUserLocation; }; favorites?: TUserFavorite[]; /** Per-skill active/inactive overrides. Key = skillId, value = active state. */ @@ -95,6 +96,7 @@ export interface UpdateUserRequest { termsAccepted?: boolean; personalization?: { memories?: boolean; + location?: import('librechat-data-provider').TUserLocation; }; skillStates?: Record; }