mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 22:25:39 +00:00
dad99bf0ee
Replies from the AI agent are now converted from markdown to HTML with goldmark before queueing, so bold, links, and lists render properly in the widget, agent app, and email. The prompt now allows simple markdown. Raw HTML in model output is escaped by goldmark, and both frontends sanitize on render anyway. Other fixes bundled in: - validate avatar type and size before creating or updating an assistant, and roll back the assistant if the avatar upload fails after create - return 404 from agent update and API key endpoints for AI assistant identity users, and hide assistants from mention and SLA user pickers - reserve the autonomous assistant's built-in tool names so custom tools cannot shadow them - apply resolve after the reply is posted so the CSAT survey follows the answer instead of preceding it - unassign the assistant on handoff even when the fallback team is the same team - count reopens by status category instead of status name, and exclude CSAT messages from the turn cap - split oversized wrapper divs into child blocks when chunking KB HTML instead of truncating them - return 404 when soft-deleting an already-deleted agent, and keep AI assistants (which have no email) visible in the compact users list
433 lines
13 KiB
SQL
433 lines
13 KiB
SQL
-- name: get-users-compact
|
|
SELECT COUNT(*) OVER() as total, users.id, users.avatar_url, users.type, users.created_at, users.updated_at, users.first_name, users.last_name, users.email, users.enabled, users.external_user_id, users.availability_status
|
|
FROM users
|
|
-- email != 'System' also drops NULL-email users (anonymous visitors); AI assistants have no email and must still be listed.
|
|
WHERE (users.email != 'System' OR users.type = 'ai_assistant') AND users.deleted_at IS NULL AND type = ANY($1)
|
|
|
|
-- name: soft-delete-agent
|
|
WITH soft_delete AS (
|
|
UPDATE users
|
|
SET deleted_at = now(), updated_at = now()
|
|
WHERE id = $1 AND type = 'agent'
|
|
RETURNING id
|
|
),
|
|
-- Delete from user_roles and teams
|
|
delete_team_members AS (
|
|
DELETE FROM team_members
|
|
WHERE user_id IN (SELECT id FROM soft_delete)
|
|
RETURNING 1
|
|
),
|
|
delete_user_roles AS (
|
|
DELETE FROM user_roles
|
|
WHERE user_id IN (SELECT id FROM soft_delete)
|
|
RETURNING 1
|
|
)
|
|
SELECT count(*) FROM soft_delete;
|
|
|
|
-- name: get-user
|
|
SELECT
|
|
u.id,
|
|
u.created_at,
|
|
u.updated_at,
|
|
u.email,
|
|
u.password,
|
|
u.type,
|
|
u.enabled,
|
|
u.avatar_url,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.availability_status,
|
|
u.last_active_at,
|
|
u.last_login_at,
|
|
u.phone_number_country_code,
|
|
u.phone_number,
|
|
u.country,
|
|
u.api_key,
|
|
u.api_key_last_used_at,
|
|
u.external_user_id,
|
|
u.api_secret,
|
|
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
|
COALESCE(
|
|
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'emoji', t.emoji))
|
|
FROM team_members tm
|
|
JOIN teams t ON tm.team_id = t.id
|
|
WHERE tm.user_id = u.id),
|
|
'[]'
|
|
) AS teams,
|
|
array_agg(DISTINCT p ORDER BY p) FILTER (WHERE p IS NOT NULL) AS permissions
|
|
FROM users u
|
|
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
|
LEFT JOIN roles r ON r.id = ur.role_id
|
|
LEFT JOIN LATERAL unnest(r.permissions) AS p ON true
|
|
WHERE u.deleted_at IS NULL
|
|
AND ($1 = 0 OR u.id = $1)
|
|
AND ($2 = '' OR u.email = $2)
|
|
AND (cardinality($3::text[]) = 0 OR u.type::text = ANY($3::text[]))
|
|
GROUP BY u.id
|
|
ORDER BY u.id ASC
|
|
LIMIT 1;
|
|
|
|
-- name: set-user-password
|
|
UPDATE users
|
|
SET password = $1, updated_at = now()
|
|
WHERE id = $2;
|
|
|
|
-- name: update-agent
|
|
WITH not_removed_roles AS (
|
|
SELECT r.id FROM unnest($5::text[]) role_name
|
|
JOIN roles r ON r.name = role_name
|
|
),
|
|
old_roles AS (
|
|
DELETE FROM user_roles
|
|
WHERE user_id = $1
|
|
AND role_id NOT IN (SELECT id FROM not_removed_roles)
|
|
),
|
|
new_roles AS (
|
|
INSERT INTO user_roles (user_id, role_id)
|
|
SELECT $1, r.id FROM not_removed_roles r
|
|
ON CONFLICT (user_id, role_id) DO NOTHING
|
|
)
|
|
UPDATE users
|
|
SET first_name = COALESCE($2, first_name),
|
|
last_name = COALESCE($3, last_name),
|
|
email = COALESCE($4, email),
|
|
avatar_url = COALESCE($6, avatar_url),
|
|
password = COALESCE($7, password),
|
|
enabled = COALESCE($8, enabled),
|
|
availability_status = COALESCE($9, availability_status),
|
|
updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: update-custom-attributes
|
|
UPDATE users
|
|
SET custom_attributes = $2,
|
|
updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: upsert-custom-attributes
|
|
UPDATE users
|
|
SET custom_attributes = COALESCE(custom_attributes, '{}'::jsonb) || $2,
|
|
updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: update-avatar
|
|
UPDATE users
|
|
SET avatar_url = $2, updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: update-availability
|
|
UPDATE users
|
|
SET availability_status = $2
|
|
WHERE id = $1;
|
|
|
|
-- name: update-last-active-at
|
|
WITH prev AS (
|
|
SELECT availability_status AS old_status FROM users WHERE id = $1
|
|
)
|
|
UPDATE users
|
|
SET last_active_at = now(),
|
|
availability_status = CASE WHEN availability_status = 'offline' THEN 'online' ELSE availability_status END
|
|
FROM prev
|
|
WHERE users.id = $1
|
|
RETURNING (prev.old_status = 'offline')::boolean AS was_offline;
|
|
|
|
-- name: update-inactive-offline
|
|
UPDATE users
|
|
SET availability_status = 'offline'
|
|
WHERE
|
|
type IN ('agent', 'contact', 'visitor')
|
|
AND (last_active_at IS NULL OR last_active_at < NOW() - INTERVAL '5 minutes')
|
|
AND availability_status NOT IN ('offline', 'away_and_reassigning', 'away_manual')
|
|
RETURNING id, type;
|
|
|
|
-- name: get-availability-status
|
|
SELECT availability_status FROM users WHERE id = $1;
|
|
|
|
-- name: set-reset-password-token
|
|
UPDATE users
|
|
SET reset_password_token = $2, reset_password_token_expiry = now() + interval '1 day'
|
|
WHERE id = $1 AND type = 'agent';
|
|
|
|
-- name: set-password
|
|
UPDATE users
|
|
SET password = $1, reset_password_token = NULL, reset_password_token_expiry = NULL
|
|
WHERE reset_password_token = $2 AND reset_password_token_expiry > now()
|
|
RETURNING id;
|
|
|
|
-- name: insert-agent
|
|
WITH inserted_user AS (
|
|
INSERT INTO users (email, type, first_name, last_name, "password", avatar_url)
|
|
VALUES ($1, 'agent', $2, $3, $4, $5)
|
|
RETURNING id AS user_id
|
|
)
|
|
INSERT INTO user_roles (user_id, role_id)
|
|
SELECT inserted_user.user_id, r.id
|
|
FROM inserted_user, unnest($6::text[]) role_name
|
|
JOIN roles r ON r.name = role_name
|
|
RETURNING user_id;
|
|
|
|
-- name: insert-contact-with-external-id
|
|
INSERT INTO users (email, type, first_name, last_name, "password", avatar_url, external_user_id, custom_attributes, phone_number, phone_number_country_code)
|
|
VALUES ($1, 'contact', $2, $3, $4, $5, $6, $7, $8, $9)
|
|
ON CONFLICT (external_user_id) WHERE type = 'contact' AND deleted_at IS NULL AND external_user_id IS NOT NULL
|
|
DO UPDATE SET email = COALESCE(NULLIF(EXCLUDED.email, ''), users.email),
|
|
first_name = COALESCE(NULLIF(EXCLUDED.first_name, ''), users.first_name),
|
|
last_name = COALESCE(NULLIF(EXCLUDED.last_name, ''), users.last_name),
|
|
phone_number = COALESCE(NULLIF(EXCLUDED.phone_number, ''), users.phone_number),
|
|
phone_number_country_code = COALESCE(NULLIF(EXCLUDED.phone_number_country_code, ''), users.phone_number_country_code),
|
|
updated_at = now()
|
|
RETURNING id;
|
|
|
|
-- name: insert-contact-without-external-id
|
|
INSERT INTO users (email, type, first_name, last_name, "password", avatar_url, external_user_id)
|
|
VALUES ($1, 'contact', $2, $3, $4, $5, NULL)
|
|
ON CONFLICT (email) WHERE type = 'contact' AND deleted_at IS NULL AND external_user_id IS NULL
|
|
DO UPDATE SET first_name = COALESCE(NULLIF(EXCLUDED.first_name, ''), users.first_name),
|
|
last_name = COALESCE(NULLIF(EXCLUDED.last_name, ''), users.last_name),
|
|
updated_at = now()
|
|
RETURNING id;
|
|
|
|
-- name: get-contact-by-email
|
|
SELECT id, external_user_id FROM users
|
|
WHERE email = $1 AND type = 'contact' AND deleted_at IS NULL
|
|
ORDER BY (external_user_id IS NOT NULL) DESC, id ASC LIMIT 1;
|
|
|
|
-- name: get-contact-by-email-without-ext-id
|
|
SELECT id FROM users
|
|
WHERE email = $1 AND type = 'contact' AND deleted_at IS NULL AND external_user_id IS NULL
|
|
LIMIT 1;
|
|
|
|
-- name: is-email-blocked
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM users
|
|
WHERE email = $1 AND type IN ('contact', 'visitor') AND deleted_at IS NULL AND enabled = false
|
|
) AS is_blocked;
|
|
|
|
-- name: set-external-user-id
|
|
UPDATE users SET external_user_id = $2, updated_at = now()
|
|
WHERE id = $1 AND type = 'contact' AND deleted_at IS NULL;
|
|
|
|
-- name: insert-visitor
|
|
INSERT INTO users (email, type, first_name, last_name, custom_attributes, phone_number, phone_number_country_code)
|
|
VALUES ($1, 'visitor', $2, $3, $4, $5, $6)
|
|
RETURNING *;
|
|
|
|
-- name: update-last-login-at
|
|
UPDATE users
|
|
SET last_login_at = now(),
|
|
updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: toggle-enable
|
|
UPDATE users
|
|
SET enabled = $3, updated_at = NOW()
|
|
WHERE id = $1 AND type = $2;
|
|
|
|
-- name: update-contact
|
|
UPDATE users
|
|
SET first_name = COALESCE($2, first_name),
|
|
last_name = COALESCE($3, last_name),
|
|
email = COALESCE($4, email),
|
|
avatar_url = $5,
|
|
phone_number = $6,
|
|
phone_number_country_code = $7,
|
|
country = $8,
|
|
updated_at = now()
|
|
WHERE id = $1 and type in ('contact', 'visitor');
|
|
|
|
-- name: update-contact-basic-info
|
|
UPDATE users
|
|
SET first_name = COALESCE(NULLIF($2, ''), first_name),
|
|
last_name = COALESCE(NULLIF($3, ''), last_name),
|
|
email = COALESCE(NULLIF($4, ''), email),
|
|
phone_number = COALESCE(NULLIF($5, ''), phone_number),
|
|
phone_number_country_code = COALESCE(NULLIF($6, ''), phone_number_country_code),
|
|
updated_at = now()
|
|
WHERE id = $1 AND type IN ('contact', 'visitor');
|
|
|
|
-- name: get-notes
|
|
SELECT
|
|
cn.id,
|
|
cn.created_at,
|
|
cn.updated_at,
|
|
cn.contact_id,
|
|
cn.note,
|
|
cn.user_id,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.avatar_url
|
|
FROM contact_notes cn
|
|
INNER JOIN users u ON u.id = cn.user_id
|
|
WHERE cn.contact_id = $1
|
|
ORDER BY cn.created_at DESC;
|
|
|
|
-- name: insert-note
|
|
INSERT INTO contact_notes (contact_id, user_id, note)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *;
|
|
|
|
-- name: delete-note
|
|
DELETE FROM contact_notes
|
|
WHERE id = $1 AND contact_id = $2;
|
|
|
|
-- name: get-note
|
|
SELECT
|
|
cn.id,
|
|
cn.created_at,
|
|
cn.updated_at,
|
|
cn.contact_id,
|
|
cn.note,
|
|
cn.user_id,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.avatar_url
|
|
FROM contact_notes cn
|
|
INNER JOIN users u ON u.id = cn.user_id
|
|
WHERE cn.id = $1;
|
|
|
|
-- name: get-user-by-api-key
|
|
SELECT
|
|
u.id,
|
|
u.created_at,
|
|
u.updated_at,
|
|
u.email,
|
|
u.password,
|
|
u.type,
|
|
u.enabled,
|
|
u.avatar_url,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.availability_status,
|
|
u.last_active_at,
|
|
u.last_login_at,
|
|
u.phone_number_country_code,
|
|
u.phone_number,
|
|
u.country,
|
|
u.api_key,
|
|
u.api_key_last_used_at,
|
|
u.api_secret,
|
|
u.external_user_id,
|
|
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
|
COALESCE(
|
|
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'emoji', t.emoji))
|
|
FROM team_members tm
|
|
JOIN teams t ON tm.team_id = t.id
|
|
WHERE tm.user_id = u.id),
|
|
'[]'
|
|
) AS teams,
|
|
array_agg(DISTINCT p ORDER BY p) FILTER (WHERE p IS NOT NULL) AS permissions
|
|
FROM users u
|
|
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
|
LEFT JOIN roles r ON r.id = ur.role_id
|
|
LEFT JOIN LATERAL unnest(r.permissions) AS p ON true
|
|
WHERE u.api_key = $1 AND u.enabled = true AND u.deleted_at IS NULL
|
|
GROUP BY u.id;
|
|
|
|
-- name: set-api-key
|
|
UPDATE users
|
|
SET api_key = $2, api_secret = $3, api_key_last_used_at = NULL, updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: revoke-api-key
|
|
UPDATE users
|
|
SET api_key = NULL, api_secret = NULL, api_key_last_used_at = NULL, updated_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: update-api-key-last-used
|
|
UPDATE users
|
|
SET api_key_last_used_at = now()
|
|
WHERE id = $1;
|
|
|
|
-- name: get-user-by-external-id
|
|
SELECT
|
|
u.id,
|
|
u.created_at,
|
|
u.updated_at,
|
|
u.email,
|
|
u.password,
|
|
u.type,
|
|
u.enabled,
|
|
u.avatar_url,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.availability_status,
|
|
u.last_active_at,
|
|
u.last_login_at,
|
|
u.phone_number_country_code,
|
|
u.phone_number,
|
|
u.country,
|
|
u.external_user_id,
|
|
u.custom_attributes,
|
|
u.api_key,
|
|
u.api_key_last_used_at,
|
|
array_agg(DISTINCT r.name) FILTER (WHERE r.name IS NOT NULL) AS roles,
|
|
COALESCE(
|
|
(SELECT json_agg(json_build_object('id', t.id, 'name', t.name, 'emoji', t.emoji))
|
|
FROM team_members tm
|
|
JOIN teams t ON tm.team_id = t.id
|
|
WHERE tm.user_id = u.id),
|
|
'[]'
|
|
) AS teams,
|
|
array_agg(DISTINCT p ORDER BY p) FILTER (WHERE p IS NOT NULL) AS permissions
|
|
FROM users u
|
|
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
|
LEFT JOIN roles r ON r.id = ur.role_id
|
|
LEFT JOIN LATERAL unnest(r.permissions) AS p ON true
|
|
WHERE u.deleted_at IS NULL
|
|
AND u.external_user_id = $1
|
|
GROUP BY u.id;
|
|
|
|
-- name: get-visitor-by-email
|
|
SELECT id, email, external_user_id FROM users
|
|
WHERE email = $1 AND type = 'visitor' AND deleted_at IS NULL
|
|
LIMIT 1;
|
|
|
|
-- name: upgrade-visitor-to-contact
|
|
UPDATE users SET type = 'contact', updated_at = now()
|
|
WHERE id = $1 AND type = 'visitor';
|
|
|
|
-- name: merge-visitor-to-contact
|
|
WITH transfer_conversations AS (
|
|
UPDATE conversations
|
|
SET contact_id = $2, updated_at = now()
|
|
WHERE contact_id = $1
|
|
RETURNING id
|
|
),
|
|
transfer_messages AS (
|
|
UPDATE conversation_messages
|
|
SET sender_id = $2
|
|
WHERE conversation_id IN (SELECT id FROM transfer_conversations) AND sender_id = $1
|
|
RETURNING id
|
|
),
|
|
transfer_participants AS (
|
|
UPDATE conversation_participants
|
|
SET user_id = $2
|
|
WHERE user_id = $1 AND NOT EXISTS (
|
|
SELECT 1 FROM conversation_participants WHERE user_id = $2 AND conversation_id = conversation_participants.conversation_id
|
|
)
|
|
RETURNING id
|
|
),
|
|
delete_remaining_participants AS (
|
|
DELETE FROM conversation_participants
|
|
WHERE user_id = $1
|
|
RETURNING id
|
|
),
|
|
transfer_notes AS (
|
|
UPDATE contact_notes
|
|
SET contact_id = $2
|
|
WHERE contact_id = $1
|
|
RETURNING id
|
|
),
|
|
delete_visitor AS (
|
|
DELETE FROM users
|
|
WHERE id = $1 AND type = 'visitor'
|
|
RETURNING id
|
|
)
|
|
SELECT
|
|
(SELECT COUNT(*) FROM transfer_conversations) as conversations_transferred,
|
|
(SELECT COUNT(*) FROM transfer_messages) as messages_transferred,
|
|
(SELECT COUNT(*) FROM delete_visitor) as visitor_deleted;
|
|
|
|
-- name: get-user-ids-by-role
|
|
SELECT user_id FROM user_roles WHERE role_id = $1;
|