Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8c16f16
feat: enhance two-factor authentication handling and add createMeteor…
ggazzo Dec 30, 2025
3b1b749
chore: remove commented-out createMeteorInvocation utility from APIClass
ggazzo Dec 30, 2025
4fa72cf
refactor(api): update starMessage method to accept user object instea…
ggazzo Dec 30, 2025
6c01de1
refactor(api): remove wrong user validation for getChannelHistory
ggazzo Dec 30, 2025
43b5abc
refactor(api): update pinMessage and unpinMessage methods to accept u…
ggazzo Dec 30, 2025
3bdc50e
refactor(api): update messageSearch method to retrieve user object di…
ggazzo Dec 30, 2025
eb50cea
refactor(api): update followMessage and unfollowMessage methods to ac…
ggazzo Dec 30, 2025
ce868ab
refactor(api): update pinMessage and unpinMessage methods to use user…
ggazzo Dec 30, 2025
9b04cd7
refactor(api): update eraseRoom function to accept user object instea…
ggazzo Dec 30, 2025
260a1a4
refactor(api): add applyMeteorContext option to API routes and update…
ggazzo Dec 30, 2025
4f992cb
refactor(apps): use route handler user instead of Meteor.userAsync()
d-gubert Dec 30, 2025
9ebc340
refactor(api): remove unused APIClass import from misc.ts for cleaner…
ggazzo Dec 30, 2025
0645bb1
refactor(api): remove unused Meteor import from rest.ts for cleaner code
ggazzo Dec 30, 2025
91ad798
refactor(api): update setEmail, setStatusText, and setUserStatus meth…
ggazzo Dec 31, 2025
d9f07aa
refactor(api): update getUserFromParams and related methods to accept…
ggazzo Dec 31, 2025
e7dd0f5
refactor(api): add user validation in saveUserProfile method to ensur…
ggazzo Dec 31, 2025
f0b058b
refactor(api): enhance twoFactorRequired and saveUserProfile methods …
ggazzo Jan 2, 2026
c2fea9d
refactor(api): update twoFactorRequired and related methods to improv…
ggazzo Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions apps/meteor/app/2fa/server/twoFactorRequired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Meteor } from 'meteor/meteor';
import type { ITwoFactorOptions } from './code/index';
import { checkCodeForUser } from './code/index';

export function twoFactorRequired<TFunction extends (this: Meteor.MethodThisType, ...args: any[]) => any>(
fn: TFunction,
export const twoFactorRequired = <TFunction extends (this: any, ...args: any) => any>(
fn: ThisParameterType<TFunction> extends Meteor.MethodThisType
? TFunction
: (this: Meteor.MethodThisType, ...args: Parameters<TFunction>) => ReturnType<TFunction>,
options?: ITwoFactorOptions,
): (this: Meteor.MethodThisType, ...args: Parameters<TFunction>) => Promise<ReturnType<TFunction>> {
return async function (this: Meteor.MethodThisType, ...args: Parameters<TFunction>): Promise<ReturnType<TFunction>> {
) =>
async function (this, ...args) {
if (!this.userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'twoFactorRequired' });
}
Expand Down Expand Up @@ -35,5 +37,4 @@ export function twoFactorRequired<TFunction extends (this: Meteor.MethodThisType
}

return fn.apply(this, args);
};
}
} as (this: ThisParameterType<TFunction>, ...args: Parameters<TFunction>) => ReturnType<TFunction>;
72 changes: 51 additions & 21 deletions apps/meteor/app/api/server/ApiClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,18 +495,16 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
public async processTwoFactor({
userId,
request,
invocation,
options,
connection,
}: {
userId: string;
request: Request;
invocation: { twoFactorChecked?: boolean };
options?: Options;
connection: IMethodConnection;
}): Promise<void> {
}): Promise<boolean> {
if (options && (!('twoFactorRequired' in options) || !options.twoFactorRequired)) {
return;
return false;
}
const code = request.headers.get('x-2fa-code') ? String(request.headers.get('x-2fa-code')) : undefined;
const method = request.headers.get('x-2fa-method') ? String(request.headers.get('x-2fa-method')) : undefined;
Expand All @@ -519,7 +517,7 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
connection,
});

invocation.twoFactorChecked = true;
return true;
}

public getFullRouteName(route: string, method: string): string {
Expand Down Expand Up @@ -901,30 +899,28 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
}
}

const invocation = new DDPCommon.MethodInvocation({
connection,
isSimulation: false,
userId: this.userId,
});

Accounts._accountData[connection.id] = {
connection,
};

Accounts._setAccountData(connection.id, 'loginToken', this.token!);

this.userId &&
if (
this.userId &&
(await api.processTwoFactor({
userId: this.userId,
request: this.request,
invocation: invocation as unknown as Record<string, any>,
options: _options,
connection: connection as unknown as IMethodConnection,
}));
}))
) {
this.twoFactorChecked = true;
}

this.parseJsonQuery = () => api.parseJsonQuery(this);

result = (await DDP._CurrentInvocation.withValue(invocation as any, async () => originalAction.apply(this))) || api.success();
if (options.applyMeteorContext) {
const invocation = APIClass.createMeteorInvocation(connection, this.userId, this.token);
result = await invocation
.applyInvocation(() => originalAction.apply(this))
.finally(() => invocation[Symbol.asyncDispose]());
} else {
result = await originalAction.apply(this);
}
} catch (e: any) {
result = ((e: any) => {
switch (e.error) {
Expand Down Expand Up @@ -1208,4 +1204,38 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
},
);
}

static createMeteorInvocation(
connection: {
id: string;
close: () => void;
clientAddress: string;
httpHeaders: Record<string, any>;
},
userId?: string,
token?: string,
) {
const invocation = new DDPCommon.MethodInvocation({
connection,
isSimulation: false,
userId,
});

Accounts._accountData[connection.id] = {
connection,
};
if (token) {
Accounts._setAccountData(connection.id, 'loginToken', token);
}

return {
invocation,
applyInvocation: <F extends () => Promise<any>>(action: F): ReturnType<F> => {
return DDP._CurrentInvocation.withValue(invocation as any, async () => action()) as ReturnType<F>;
},
[Symbol.asyncDispose]() {
return Promise.resolve();
},
};
}
}
3 changes: 3 additions & 0 deletions apps/meteor/app/api/server/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export type SharedOptions<TMethod extends string> = (
version: DeprecationLoggerNextPlannedVersion;
alternatives?: PathPattern[];
};
applyMeteorContext?: boolean;
};

export type GenericRouteExecutionContext = ActionThis<any, any, any>;
Expand Down Expand Up @@ -180,6 +181,8 @@ export type ActionThis<TMethod extends Method, TPathPattern extends PathPattern,
readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never;
readonly queryFields: TOptions extends { queryFields: infer T } ? T : never;

readonly twoFactorChecked: boolean;

parseJsonQuery(): Promise<{
sort: Record<string, 1 | -1>;
/**
Expand Down
6 changes: 1 addition & 5 deletions apps/meteor/app/api/server/helpers/getUserFromParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

export async function getUserFromParams(params: {
userId?: string;
username?: string;
user?: string;
}): Promise<Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'statusText' | 'roles'>> {
export async function getUserFromParams(params: { userId?: string; username?: string; user?: string }) {
let user;

const projection = { username: 1, name: 1, status: 1, statusText: 1, roles: 1 };
Expand Down
14 changes: 7 additions & 7 deletions apps/meteor/app/api/server/lib/eraseTeam.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
import { MeteorError, Team } from '@rocket.chat/core-services';
import type { AtLeast, IRoom, ITeam, IUser } from '@rocket.chat/core-typings';
import type { IRoom, ITeam, IUser } from '@rocket.chat/core-typings';
import { Rooms } from '@rocket.chat/models';

import { eraseRoom } from '../../../../server/lib/eraseRoom';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { deleteRoom } from '../../../lib/server/functions/deleteRoom';

type eraseRoomFnType = (rid: string, user: AtLeast<IUser, '_id' | 'username' | 'name'>) => Promise<boolean | void>;
type eraseRoomFnType = <T extends IUser>(rid: string, user: T) => Promise<boolean | void>;

export const eraseTeamShared = async (
user: AtLeast<IUser, '_id' | 'username' | 'name'>,
export const eraseTeamShared = async <T extends IUser>(
user: T,
team: ITeam,
roomsToRemove: IRoom['_id'][] = [],
eraseRoomFn: eraseRoomFnType,
Expand Down Expand Up @@ -41,9 +41,9 @@ export const eraseTeamShared = async (
await Team.deleteById(team._id);
};

export const eraseTeam = async (user: AtLeast<IUser, '_id' | 'username' | 'name'>, team: ITeam, roomsToRemove: IRoom['_id'][]) => {
export const eraseTeam = async (user: IUser, team: ITeam, roomsToRemove: IRoom['_id'][]) => {
await eraseTeamShared(user, team, roomsToRemove, async (rid, user) => {
return eraseRoom(rid, user._id);
return eraseRoom(rid, user);
});
};

Expand All @@ -54,7 +54,7 @@ export const eraseTeam = async (user: AtLeast<IUser, '_id' | 'username' | 'name'
*/
export const eraseTeamOnRelinquishRoomOwnerships = async (team: ITeam, roomsToRemove: IRoom['_id'][] = []): Promise<string[]> => {
const deletedRooms = new Set<string>();
await eraseTeamShared({ _id: 'rocket.cat', username: 'rocket.cat', name: 'Rocket.Cat' }, team, roomsToRemove, async (rid) => {
await eraseTeamShared({ _id: 'rocket.cat', username: 'rocket.cat', name: 'Rocket.Cat' } as IUser, team, roomsToRemove, async (rid) => {
const isDeleted = await eraseRoomLooseValidation(rid);
if (isDeleted) {
deletedRooms.add(rid);
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ API.v1.addRoute(
checkedArchived: false,
});

await eraseRoom(room._id, this.userId);
await eraseRoom(room._id, this.user);

return API.v1.success();
},
Expand Down
12 changes: 6 additions & 6 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ const chatEndpoints = API.v1
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

const pinnedMessage = await pinMessage(msg, this.userId);
const pinnedMessage = await pinMessage(msg, this.user);

const [message] = await normalizeMessagesForUser([pinnedMessage], this.userId);

Expand Down Expand Up @@ -275,7 +275,7 @@ const chatEndpoints = API.v1
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

await unpinMessage(this.userId, msg);
await unpinMessage(this.user, msg);

return API.v1.success();
},
Expand Down Expand Up @@ -456,7 +456,7 @@ API.v1.addRoute(
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

await starMessage(this.userId, {
await starMessage(this.user, {
_id: msg._id,
rid: msg.rid,
starred: true,
Expand All @@ -478,7 +478,7 @@ API.v1.addRoute(
throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.');
}

await starMessage(this.userId, {
await starMessage(this.user, {
_id: msg._id,
rid: msg.rid,
starred: false,
Expand Down Expand Up @@ -804,7 +804,7 @@ API.v1.addRoute(
throw new Meteor.Error('The required "mid" body param is missing.');
}

await followMessage(this.userId, { mid });
await followMessage(this.user, { mid });

return API.v1.success();
},
Expand All @@ -822,7 +822,7 @@ API.v1.addRoute(
throw new Meteor.Error('The required "mid" body param is missing.');
}

await unfollowMessage(this.userId, { mid });
await unfollowMessage(this.user, { mid });

return API.v1.success();
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ API.v1.addRoute(
checkedArchived: false,
});

await eraseRoom(findResult.rid, this.userId);
await eraseRoom(findResult.rid, this.user);

return API.v1.success();
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/im.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const dmDeleteAction = <Path extends string>(_path: Path): TypedAction<typeof dm
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

await eraseRoom(room._id, this.userId);
await eraseRoom(room._id, this.user);

return API.v1.success();
};
Expand Down
9 changes: 5 additions & 4 deletions apps/meteor/app/api/server/v1/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ API.v1.addRoute(
authRequired: true,
rateLimiterOptions: false,
validateParams: isMeteorCall,
applyMeteorContext: true,
},
{
async post() {
Expand Down Expand Up @@ -515,8 +516,7 @@ API.v1.addRoute(
});
}

const result = await Meteor.callAsync(method, ...params);
return API.v1.success(mountResult({ id, result }));
return API.v1.success(mountResult({ id, result: await Meteor.callAsync(method, ...params) }));
} catch (err) {
if (!(err as any).isClientSafe && !(err as any).meteorError) {
SystemLogger.error({ msg: `Exception while invoking method ${method}`, err });
Expand All @@ -530,12 +530,14 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'method.callAnon/:method',
{
authRequired: false,
rateLimiterOptions: false,
validateParams: isMeteorCall,
applyMeteorContext: true,
},
{
async post() {
Expand Down Expand Up @@ -571,8 +573,7 @@ API.v1.addRoute(
});
}

const result = await Meteor.callAsync(method, ...params);
return API.v1.success(mountResult({ id, result }));
return API.v1.success(mountResult({ id, result: await Meteor.callAsync(method, ...params) }));
} catch (err) {
if (!(err as any).isClientSafe && !(err as any).meteorError) {
SystemLogger.error({ msg: `Exception while invoking method ${method}`, err });
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ API.v1.addRoute(
});
}

await eraseRoom(room, this.userId);
await eraseRoom(room, this.user);

return API.v1.success();
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ API.v1.addRoute(

if (rooms.length) {
for await (const room of rooms) {
await eraseRoom(room, this.userId);
await eraseRoom(room, this.user);
}
}

Expand Down
11 changes: 4 additions & 7 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/persona
import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken';
import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged';
import { i18n } from '../../../../server/lib/i18n';
import { removeOtherTokens } from '../../../../server/lib/removeOtherTokens';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { registerUser } from '../../../../server/methods/registerUser';
import { requestDataDownload } from '../../../../server/methods/requestDataDownload';
Expand Down Expand Up @@ -181,7 +180,7 @@ API.v1.addRoute(
};

await executeSaveUserProfile.call(
this as unknown as Meteor.MethodThisType,
this as unknown as Meteor.MethodThisType & { token: string },
this.user,
userData,
this.bodyParams.customFields,
Expand Down Expand Up @@ -1232,7 +1231,7 @@ API.v1.addRoute(
{ authRequired: true },
{
async post() {
return API.v1.success(await removeOtherTokens(this.userId, this.connection.id));
return API.v1.success(await Users.removeNonLoginTokensExcept(this.userId, this.token));
},
},
);
Expand Down Expand Up @@ -1397,9 +1396,7 @@ API.v1.addRoute(
});
}

const user = await (async (): Promise<
Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'statusText' | 'roles'> | undefined | null
> => {
const user = await (async () => {
if (isUserFromParams(this.bodyParams, this.userId, this.user)) {
return Users.findOneById(this.userId);
}
Expand All @@ -1416,7 +1413,7 @@ API.v1.addRoute(
let { statusText, status } = user;

if (this.bodyParams.message || this.bodyParams.message === '') {
await setStatusText(user._id, this.bodyParams.message, { emit: false });
await setStatusText(user, this.bodyParams.message, { emit: false });
statusText = this.bodyParams.message;
}

Expand Down
Loading
Loading