Files
libredesk/internal/user/queries.sql
T
Abhinav Raut 29881f2f82 fix: close ws stale-perm gaps and resub on reconnect
Reject disabled agents in FilterAuthorizedListUUIDs, kick role members on
permission removal or role delete, and re-sub the list and open conv on
ws reconnect using local state. Also plug the app logger into the ws
package so kicks no longer log when no connections exist.
2026-05-28 20:07:19 +05:30

423 lines
12 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
WHERE users.email != 'System' 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 1;
-- 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)
VALUES ($1, 'contact', $2, $3, $4, $5, $6, $7)
ON CONFLICT (external_user_id) WHERE type = 'contact' AND deleted_at IS NULL AND external_user_id IS NOT NULL
DO UPDATE SET email = EXCLUDED.email, first_name = EXCLUDED.first_name, last_name = EXCLUDED.last_name, 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 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)
VALUES ($1, 'visitor', $2, $3, $4)
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),
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;