RocketChat/Rocket.Chat · error · Error

error-not-authorized

Error message

error-not-authorized

What it means

findBusinessHours is the Enterprise helper behind the livechat business-hours listing API (apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts). It gates on the 'view-livechat-business-hours' permission via hasPermissionAsync and throws a plain Error('error-not-authorized') before the paginated LivechatBusinessHours query ever runs. The throw means the authenticated user's role set does not include that specific permission; other livechat permissions do not cover it.

Solutions

  1. Grant 'view-livechat-business-hours' to the caller's role under Administration > Permissions (it is normally held by admin and livechat-manager style roles).
  2. When calling findBusinessHours server-side, verify you are passing the authenticated requester's userId, not a system/undefined id.
  3. If wrapping this helper in a custom route, map Error('error-not-authorized') to HTTP 403 so clients get a meaningful status instead of a generic 500.

Example fix

// before
const { businessHours } = await findBusinessHours(uid, { offset, count }); // 500-ish raw error for under-privileged callers

// after - pre-check the same permission in the calling route
if (!(await hasPermissionAsync(uid, 'view-livechat-business-hours'))) {
	return API.v1.unauthorized();
}
const { businessHours } = await findBusinessHours(uid, { offset, count });
Defensive patterns

Strategy: validation

Validate before calling

import { hasPermissionAsync } from '../../../../server/lib/authorization/hasPermission';

if (!(await hasPermissionAsync(userId, 'view-livechat-business-hours'))) {
	// skip the call or return 403; do not invoke findBusinessHours
}

Try / catch

try {
	const res = await findBusinessHours(userId, pagination, name);
} catch (e: any) {
	if (e instanceof Error && e.message === 'error-not-authorized') return API.v1.unauthorized(); // 403
	throw e;
}

Prevention

When it happens

Trigger: Calling GET livechat/business-hours (which delegates to findBusinessHours(userId, pagination, name)) with a session whose roles lack 'view-livechat-business-hours' - e.g. a livechat-agent, a plain user, or a userId resolving to no roles. The check is the first statement, so pagination/sort/name filter parameters are irrelevant; authorization alone decides.

Common situations: A custom 'business hours manager' role was created without the permission; testing the endpoint with an agent or manager account whose role lacks it; permissions trimmed or reset during a workspace migration/upgrade; assuming 'view-livechat-manager' or admin-adjacent roles cover business hours (they do not - the permission is separate).

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@2a7de45707 (2026-08-18). Data as JSON: /api/errors/0ed87b989c030ef1. Report an issue: GitHub.

Appendix: source

Thrown at apps/meteor/ee/server/lib/omnichannel/business-hour/lib/business-hour.ts:14

import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, LivechatDepartment } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/tools';

import { hasPermissionAsync } from '../../../../../../server/lib/authorization/hasPermission';
import type { IPaginatedResponse, IPagination } from '../../../../api/v1/omnichannel/lib/definition';

interface IResponse extends IPaginatedResponse {
	businessHours: ILivechatBusinessHour[];
}

export async function findBusinessHours(userId: string, { offset, count, sort }: IPagination, name?: string): Promise<IResponse> {
	if (!(await hasPermissionAsync(userId, 'view-livechat-business-hours'))) {
		throw new Error('error-not-authorized');
	}
	const query = {};
	if (name) {
		const filterReg = new RegExp(escapeRegExp(name), 'i');
		Object.assign(query, { name: filterReg });
	}
	const { cursor, totalCount } = LivechatBusinessHours.findPaginated(query, {
		sort: sort || { name: 1 },
		skip: offset,
		limit: count,
	});

	const [businessHours, total] = await Promise.all([cursor.toArray(), totalCount]);

	// add departments to businessHours
	const businessHoursWithDepartments = await Promise.all(
		businessHours.map(async (businessHour) => {
			const currentDepartments = await LivechatDepartment.findByBusinessHourId(businessHour._id, {

View on GitHub (pinned to 2a7de45707)