Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 20 additions & 2 deletions packages/db/src/services/profile.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,24 @@ interface GetProfileListOptions {
isExternal?: boolean;
}

// Single-token identifiers (firebase uids, uuids, cuids): no spaces or "@",
// 16+ chars. ID-shaped searches resolve via the (project_id, id) primary key
// instead of ILIKE-scanning email/name columns, which times out on projects
// with tens of millions of profiles.
export function looksLikeProfileId(search: string) {
return /^[A-Za-z0-9_.:-]{16,}$/.test(search);
}

function getProfileSearchWhereClause(search: string) {
if (looksLikeProfileId(search)) {
// Prefix LIKE (no leading wildcard) is primary-key-prunable and also
// covers exact matches. The id regex excludes LIKE metacharacters.
return `id LIKE ${sqlstring.escape(`${search}%`)}`;
}
const pattern = sqlstring.escape(`%${search}%`);
return `(email ILIKE ${pattern} OR first_name ILIKE ${pattern} OR last_name ILIKE ${pattern})`;
}

Comment on lines +157 to +174

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant code and inspect the surrounding implementations.
git ls-files packages/db/src/services/profile.service.ts packages/db/src/services/session.service.ts \
  | xargs -r -I{} sh -c 'echo "===== {} ====="; nl -ba "{}" | sed -n "130,220p"'

# Find the helper/library used for escaping.
rg -n "sqlstring\.escape|looksLikeProfileId|getProfileSearchWhereClause|profile_id|ILIKE|LIKE" packages/db/src/services -S

Repository: Dashverse/openpanel-forked

Length of output: 47088


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact search-building code and nearby usages.
for f in packages/db/src/services/profile.service.ts packages/db/src/services/session.service.ts; do
  echo "===== $f ====="
  nl -ba "$f" | sed -n '130,230p'
done

echo "===== search for escaping / LIKE usage ====="
rg -n "sqlstring\.escape|looksLikeProfileId|getProfileSearchWhereClause|profile_id|ILIKE|LIKE" packages/db/src/services -S

Repository: Dashverse/openpanel-forked

Length of output: 260


Escape LIKE wildcards in ID searches. looksLikeProfileId allows _, but id LIKE ... treats it as a wildcard; sqlstring.escape does not escape LIKE metacharacters, so IDs such as user_12345 can match extra profiles. Apply the same fix in packages/db/src/services/session.service.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/services/profile.service.ts` around lines 157 - 174, The
ID-search branch in getProfileSearchWhereClause treats `_` as a SQL LIKE
wildcard because sqlstring.escape only quotes the string and does not escape
LIKE metacharacters. Update the ID-prefix query in profile.service.ts to escape
LIKE wildcards before appending the trailing % so exact ID-shaped searches like
user_12345 do not overmatch, and apply the same fix to the analogous search
logic in session.service.ts.

export async function getProfiles(ids: string[], projectId: string) {
const filteredIds = uniq(ids.filter((id) => id !== ''));

Expand Down Expand Up @@ -202,7 +220,7 @@ export async function getProfileList({
sb.offset = Math.max(0, (cursor ?? 0) * take);
sb.orderBy.created_at = 'created_at DESC';
if (search) {
sb.where.search = `(email ILIKE '%${search}%' OR first_name ILIKE '%${search}%' OR last_name ILIKE '%${search}%')`;
sb.where.search = getProfileSearchWhereClause(search);
}
if (isExternal !== undefined) {
sb.where.external = `is_external = ${isExternal ? 'true' : 'false'}`;
Expand All @@ -222,7 +240,7 @@ export async function getProfileListCount({
sb.where.project_id = `project_id = ${sqlstring.escape(projectId)}`;
sb.groupBy.project_id = 'project_id';
if (search) {
sb.where.search = `(email ILIKE '%${search}%' OR first_name ILIKE '%${search}%' OR last_name ILIKE '%${search}%')`;
sb.where.search = getProfileSearchWhereClause(search);
}
if (isExternal !== undefined) {
sb.where.external = `is_external = ${isExternal ? 'true' : 'false'}`;
Expand Down
21 changes: 17 additions & 4 deletions packages/db/src/services/session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { clix } from '../clickhouse/query-builder';
import { createSqlBuilder } from '../sql-builder';
import { getEventFiltersWhereClause } from './chart.service';
import { getOrganizationByProjectIdCached } from './organization.service';
import { type IServiceProfile, getProfilesCached } from './profile.service';
import {
type IServiceProfile,
getProfilesCached,
looksLikeProfileId,
} from './profile.service';

export type IClickhouseSession = {
id: string;
Expand Down Expand Up @@ -179,8 +183,12 @@ export async function getSessionList({
if (profileId)
sb.where.profileId = `profile_id = ${sqlstring.escape(profileId)}`;
if (search) {
const s = sqlstring.escape(`%${search}%`);
sb.where.search = `(entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`;
if (looksLikeProfileId(search)) {
sb.where.search = `profile_id LIKE ${sqlstring.escape(`${search}%`)}`;
} else {
const s = sqlstring.escape(`%${search}%`);
sb.where.search = `(entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`;
}
}
if (filters?.length) {
Object.assign(sb.where, getEventFiltersWhereClause(filters));
Expand Down Expand Up @@ -304,7 +312,12 @@ export async function getSessionsCount({
}

if (search) {
sb.where.search = `(entry_path ILIKE '%${search}%' OR exit_path ILIKE '%${search}%' OR referrer ILIKE '%${search}%' OR referrer_name ILIKE '%${search}%')`;
if (looksLikeProfileId(search)) {
sb.where.search = `profile_id LIKE ${sqlstring.escape(`${search}%`)}`;
} else {
const s = sqlstring.escape(`%${search}%`);
sb.where.search = `(entry_path ILIKE ${s} OR exit_path ILIKE ${s} OR referrer ILIKE ${s} OR referrer_name ILIKE ${s})`;
}
}

if (filters && filters.length > 0) {
Expand Down
Loading