feat: migrate IM identity and snowflake primary keys
This commit is contained in:
+2
-2
@@ -395,7 +395,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
|||||||
|
|
||||||
- 获取验证码:调用 `POST /v1/im/auth/verification-codes`,必须带 `X-Tinode-APIKey`、`X-Tenant-Code`、`X-IM-Schema: 1`,Body `{"scene":"register","phone":"+86138..."}`;成功返回 `challenge_id`、`expires_in_seconds`、`resend_after_seconds`。
|
- 获取验证码:调用 `POST /v1/im/auth/verification-codes`,必须带 `X-Tinode-APIKey`、`X-Tenant-Code`、`X-IM-Schema: 1`,Body `{"scene":"register","phone":"+86138..."}`;成功返回 `challenge_id`、`expires_in_seconds`、`resend_after_seconds`。
|
||||||
- 提交注册:前端校验密码一致和协议后,先调用 `POST /v1/im/auth/verification-codes/{challenge_id}/verify`,Body `{"code":"123456"}`,获得一次性 `verification_token`。
|
- 提交注册:前端校验密码一致和协议后,先调用 `POST /v1/im/auth/verification-codes/{challenge_id}/verify`,Body `{"code":"123456"}`,获得一次性 `verification_token`。
|
||||||
- WebSocket:已完成同一企业 `hi` 后发送 `{acc}`。把一次性验证结果放在 `{acc}.cred[0].resp`,它必须是 HTTP verify 返回的 `verification_token`,不是短信验证码 `123456`。不得写入 `desc.private`,也不得在 `{acc}` 顶层传 `tags`。服务端会根据已校验的 `cred` 建立手机号凭证;客户端主动设置 `tags:["tel:..."]` 会被 Tinode 权限策略拒绝并返回 `403 params.what=tags`。
|
- WebSocket:已完成同一企业 `hi` 后发送 `{acc}`。把一次性验证结果放在 `{acc}.cred[0].resp`,它必须是 HTTP verify 返回的 `verification_token`,不是短信验证码 `123456`。不得写入 `desc.private`,也不得在 `{acc}` 顶层传 `tags`。服务端会把已校验手机号写入 `im_users.phone`,并保留验证凭证记录;用户标签已废弃,客户端设置 `tags` 会返回 `403 params.what=tags`。
|
||||||
- 手机号格式:HTTP 发码、验证码校验、`{acc}.secret` 中的 username、`cred.val` 必须使用同一个规范化号码,推荐 E.164 格式如 `+86138...`;不要一处使用 `+86138...`、另一处使用 `86138...`。
|
- 手机号格式:HTTP 发码、验证码校验、`{acc}.secret` 中的 username、`cred.val` 必须使用同一个规范化号码,推荐 E.164 格式如 `+86138...`;不要一处使用 `+86138...`、另一处使用 `86138...`。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -1679,7 +1679,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
|||||||
|
|
||||||
**状态:Tinode `me` 资料更新已有基础;目标产品接口需改造。**
|
**状态:Tinode `me` 资料更新已有基础;目标产品接口需改造。**
|
||||||
|
|
||||||
- 查询 `GET /v1/im/me/profile` 返回 `avatar_file_id,nickname,birth_date,signature,gender,region,public_account,version,field_rules`。
|
- 查询 `GET /v1/im/me/profile` 返回 `avatar_file_id,nickname,birth_date,signature,gender,region,email,public_account,version,field_rules`;`email` 当前仅为资料字段,不用于登录或找回密码。
|
||||||
- 更新统一使用 `PATCH /v1/im/me/profile`,仅提交变更字段并带 `version`;服务端更新后同步 Tinode `me.public` 并推送资料事件。
|
- 更新统一使用 `PATCH /v1/im/me/profile`,仅提交变更字段并带 `version`;服务端更新后同步 Tinode `me.public` 并推送资料事件。
|
||||||
- 头像先以 `purpose=user_avatar` 上传再 PATCH;任何字段的可见性由隐私策略裁剪。
|
- 头像先以 `purpose=user_avatar` 上传再 PATCH;任何字段的可见性由隐私策略裁剪。
|
||||||
|
|
||||||
|
|||||||
+32
-7
@@ -7,6 +7,7 @@
|
|||||||
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
|
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
|
||||||
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
|
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
|
||||||
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
|
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
|
||||||
|
-- 5. Non-auto-increment BIGINT primary keys are generated by the server Snowflake generator.
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
@@ -54,12 +55,14 @@ CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
|
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
auth_version INT NOT NULL DEFAULT 1,
|
auth_version INT NOT NULL DEFAULT 1,
|
||||||
password_updated_at DATETIME(3),
|
password_updated_at DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (user_id)
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_user_security_user (user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
|
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
|
||||||
@@ -125,6 +128,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
|
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
purpose VARCHAR(64) NOT NULL,
|
purpose VARCHAR(64) NOT NULL,
|
||||||
password_hash VARBINARY(255) NOT NULL,
|
password_hash VARBINARY(255) NOT NULL,
|
||||||
@@ -136,7 +140,8 @@ CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
|
|||||||
locked_until DATETIME(3),
|
locked_until DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (user_id, purpose)
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_youth_mode_settings_user_purpose (user_id, purpose)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
@@ -234,6 +239,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
|
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
peer_id BIGINT NOT NULL,
|
peer_id BIGINT NOT NULL,
|
||||||
status VARCHAR(16) NOT NULL,
|
status VARCHAR(16) NOT NULL,
|
||||||
@@ -241,16 +247,19 @@ CREATE TABLE IF NOT EXISTS tinode.im_friendships (
|
|||||||
topic CHAR(25) NOT NULL DEFAULT '',
|
topic CHAR(25) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (user_id, peer_id),
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_friendships_pair (user_id, peer_id),
|
||||||
INDEX idx_im_friendships_peer (peer_id, status)
|
INDEX idx_im_friendships_peer (peer_id, status)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
|
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
blocker_id BIGINT NOT NULL,
|
blocker_id BIGINT NOT NULL,
|
||||||
blocked_id BIGINT NOT NULL,
|
blocked_id BIGINT NOT NULL,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (blocker_id, blocked_id),
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_user_blocks_pair (blocker_id, blocked_id),
|
||||||
INDEX idx_im_user_blocks_blocked (blocked_id)
|
INDEX idx_im_user_blocks_blocked (blocked_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
@@ -553,6 +562,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
|
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
@@ -560,14 +570,26 @@ CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
|
|||||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (user_id)
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_user_profiles_user (user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
-- Existing database migration (the application performs the same migration
|
||||||
|
-- idempotently at startup):
|
||||||
|
-- 1. Add im_users.phone VARCHAR(32) NULL and a UNIQUE index.
|
||||||
|
-- 2. Add im_user_profiles.email VARCHAR(128) NOT NULL DEFAULT ''.
|
||||||
|
-- 3. Backfill phone/email from validated im_credentials first, then use
|
||||||
|
-- tel:/email: rows in im_usertags as a legacy fallback.
|
||||||
|
-- 4. Drop im_usertags and im_users.tags only after the backfill is verified.
|
||||||
|
-- im_topics.tags and im_topictags are topic discovery data and must be retained.
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
add_me_methods JSON,
|
add_me_methods JSON,
|
||||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
@@ -576,7 +598,8 @@ CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
|||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (user_id)
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_user_privacy_settings_user (user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
|
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
|
||||||
@@ -599,6 +622,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
|
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
device_id VARCHAR(128) NOT NULL,
|
device_id VARCHAR(128) NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
provider VARCHAR(32) NOT NULL,
|
provider VARCHAR(32) NOT NULL,
|
||||||
@@ -607,7 +631,8 @@ CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
|
|||||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (device_id),
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_device_push_tokens_device (device_id),
|
||||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
|
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
|
||||||
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
|
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|||||||
@@ -127,6 +127,8 @@
|
|||||||
9. 好友/黑名单写操作已统一接入 HTTP 幂等、关系事件和必要的对端系统通知。
|
9. 好友/黑名单写操作已统一接入 HTTP 幂等、关系事件和必要的对端系统通知。
|
||||||
10. 用户搜索和好友申请已执行 `add_me_methods`,手机号搜索不会返回禁止该方式的非好友。
|
10. 用户搜索和好友申请已执行 `add_me_methods`,手机号搜索不会返回禁止该方式的非好友。
|
||||||
11. 好友个人主页已执行 `profile_visibility`。
|
11. 好友个人主页已执行 `profile_visibility`。
|
||||||
|
12. 用户搜索已改用昵称、公开账号、登录账号和 `im_users.phone` 等结构化字段,不再依赖用户标签。
|
||||||
|
13. 注册手机号以 `im_users.phone` 为真源,邮箱作为 `im_user_profiles.email` 资料字段;旧 `im_users.tags/im_usertags` 已迁移删除,Topic 标签保留。
|
||||||
|
|
||||||
### 5.2 后续增强项
|
### 5.2 后续增强项
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
-- 适用目标:已有 Tinode MySQL 库,补齐本项目 1-7 章业务层依赖表与关键字段。
|
-- 适用目标:已有 Tinode MySQL 库,补齐本项目 1-7 章业务层依赖表与关键字段。
|
||||||
-- 注意:本稿使用 MySQL 8 functional index 处理空 idempotency_key 唯一性。
|
-- 注意:本稿使用 MySQL 8 functional index 处理空 idempotency_key 唯一性。
|
||||||
-- 第一阶段多库方案:本 SQL 的 im_tenant 放 hotgo 公共库;tinode/各租户业务库表暂时保留 tenant_id 字段。
|
-- 第一阶段多库方案:本 SQL 的 im_tenant 放 hotgo 公共库;tinode/各租户业务库表暂时保留 tenant_id 字段。
|
||||||
|
-- 本文件中的非自增 BIGINT 主键由服务端雪花算法生成。
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
@@ -79,13 +80,15 @@ CREATE TABLE IF NOT EXISTS im_verification_codes(
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS im_user_security(
|
CREATE TABLE IF NOT EXISTS im_user_security(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
auth_version INT NOT NULL DEFAULT 1,
|
auth_version INT NOT NULL DEFAULT 1,
|
||||||
password_updated_at DATETIME(3),
|
password_updated_at DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(tenant_id,user_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_security_tenant_user(tenant_id,user_id),
|
||||||
CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
@@ -132,6 +135,7 @@ CREATE TABLE IF NOT EXISTS im_action_tokens(
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
|
CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
purpose VARCHAR(64) NOT NULL,
|
purpose VARCHAR(64) NOT NULL,
|
||||||
@@ -140,7 +144,8 @@ CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
|
|||||||
locked_until DATETIME(3),
|
locked_until DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(tenant_id, user_id, purpose),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_youth_settings_tenant_user_purpose(tenant_id, user_id, purpose),
|
||||||
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
@@ -192,6 +197,7 @@ CREATE UNIQUE INDEX uk_im_friend_requests_idempotency
|
|||||||
ON im_friend_requests(tenant_id, applicant_id, (NULLIF(idempotency_key, '')));
|
ON im_friend_requests(tenant_id, applicant_id, (NULLIF(idempotency_key, '')));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS im_friendships(
|
CREATE TABLE IF NOT EXISTS im_friendships(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
peer_id BIGINT NOT NULL,
|
peer_id BIGINT NOT NULL,
|
||||||
@@ -200,18 +206,21 @@ CREATE TABLE IF NOT EXISTS im_friendships(
|
|||||||
topic CHAR(25) NOT NULL DEFAULT '',
|
topic CHAR(25) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(tenant_id, user_id, peer_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_friendships_tenant_pair(tenant_id, user_id, peer_id),
|
||||||
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
|
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
|
||||||
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS im_user_blocks(
|
CREATE TABLE IF NOT EXISTS im_user_blocks(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
tenant_id BIGINT NOT NULL,
|
tenant_id BIGINT NOT NULL,
|
||||||
blocker_id BIGINT NOT NULL,
|
blocker_id BIGINT NOT NULL,
|
||||||
blocked_id BIGINT NOT NULL,
|
blocked_id BIGINT NOT NULL,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(tenant_id, blocker_id, blocked_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_blocks_tenant_pair(tenant_id, blocker_id, blocked_id),
|
||||||
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
|
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
|
||||||
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
@@ -312,5 +321,6 @@ CREATE TABLE IF NOT EXISTS im_message_revisions(
|
|||||||
-- ALTER TABLE im_filemsglinks ADD COLUMN userid BIGINT AFTER topic;
|
-- ALTER TABLE im_filemsglinks ADD COLUMN userid BIGINT AFTER topic;
|
||||||
-- CREATE INDEX im_filemsglinks_tenant_fileid ON im_filemsglinks(tenant_id, fileid);
|
-- CREATE INDEX im_filemsglinks_tenant_fileid ON im_filemsglinks(tenant_id, fileid);
|
||||||
|
|
||||||
-- im_usertags/im_topictags/im_devices/im_auth/im_credentials 也已按租户增加 tenant_id;
|
-- im_usertags 已废弃;im_users.phone 是手机号真源,邮箱存放于 im_user_profiles.email。
|
||||||
|
-- im_topictags/im_devices/im_auth/im_credentials 也已按租户增加 tenant_id;
|
||||||
-- 若旧库缺字段,需要参照当前 server/db/mysql/schema.sql 逐项补齐。
|
-- 若旧库缺字段,需要参照当前 server/db/mysql/schema.sql 逐项补齐。
|
||||||
|
|||||||
+7
-1
@@ -205,7 +205,7 @@ HTTP API 通用规则:
|
|||||||
| 模块 | 目标表 | 主要职责 |
|
| 模块 | 目标表 | 主要职责 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 好友关系 | `im_friendships`、`im_friend_requests`、`im_user_blocks` | 好友申请、同意、拒绝、删除、拉黑 |
|
| 好友关系 | `im_friendships`、`im_friend_requests`、`im_user_blocks` | 好友申请、同意、拒绝、删除、拉黑 |
|
||||||
| 用户资料 | `im_user_profiles`、`im_user_privacy_settings` | 昵称、生日、签名、可见性和添加方式 |
|
| 用户资料 | `im_users`、`im_user_profiles`、`im_user_privacy_settings` | 注册手机号、昵称、邮箱、生日、签名、可见性和添加方式 |
|
||||||
| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_announcements`、`im_group_mutes`、`im_group_admin_logs` | 入群审批、公告、禁言、管理员、转让群主 |
|
| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_announcements`、`im_group_mutes`、`im_group_admin_logs` | 入群审批、公告、禁言、管理员、转让群主 |
|
||||||
| 群文件 | `im_group_files` | 群文件目录、搜索、删除权限 |
|
| 群文件 | `im_group_files` | 群文件目录、搜索、删除权限 |
|
||||||
| 收藏 | `im_favorites` | 收藏去重、分类、来源、搜索和分享 |
|
| 收藏 | `im_favorites` | 收藏去重、分类、来源、搜索和分享 |
|
||||||
@@ -234,6 +234,10 @@ HTTP API 通用规则:
|
|||||||
| 群成员 | `im_group_members` | 群成员业务状态、角色、入群来源和操作者 |
|
| 群成员 | `im_group_members` | 群成员业务状态、角色、入群来源和操作者 |
|
||||||
| 消息版本 | `im_message_revisions` | 编辑前内容、版本、操作者和变更原因 |
|
| 消息版本 | `im_message_revisions` | 编辑前内容、版本、操作者和变更原因 |
|
||||||
|
|
||||||
|
MySQL 表主键约定:所有 `im_*` 基础表必须具有独立的 `id` 主键。`im_user_security`、`im_kvmeta`、用户资料/隐私、设备 Push Token、好友/黑名单、青少年模式以及群设置/公告已读/禁言等原先使用自然键或复合主键的表,统一改为服务端雪花算法生成的 `BIGINT id` 主键;原业务键保留唯一索引,查询和幂等语义不变。服务启动时会对当前租户库执行幂等迁移,为存量行补齐雪花 ID 后再替换主键。迁移期间不得同时运行不写入 `id` 的旧版本服务。
|
||||||
|
|
||||||
|
用户身份字段约定:`im_users.phone` 是注册、密码找回和敏感操作验证使用的手机号真源,新注册必须提供已验证手机号;历史无手机号账号迁移后允许暂时为 `NULL`。`im_user_profiles.email` 仅是未验证的资料字段,不参与登录和密码找回。旧 `im_users.tags` 与 `im_usertags` 已废弃,启动迁移会先从已验证凭证和旧标签回填手机号/邮箱,再删除两者。群组发现仍使用 `im_topics.tags` 与 `im_topictags`,不受本次调整影响。
|
||||||
|
|
||||||
### 7.3 企业码与登录流程
|
### 7.3 企业码与登录流程
|
||||||
|
|
||||||
1. 未认证客户端先通过 `hi.tenant` 提交企业码;服务端只按 `im_tenant.code` 查找启用租户并把 `TenantID` 固定到 Session。
|
1. 未认证客户端先通过 `hi.tenant` 提交企业码;服务端只按 `im_tenant.code` 查找启用租户并把 `TenantID` 固定到 Session。
|
||||||
@@ -506,6 +510,8 @@ end_reason, created_at, updated_at
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 头像、昵称、签名 | User Public + `im_user_profiles` |
|
| 头像、昵称、签名 | User Public + `im_user_profiles` |
|
||||||
| 生日、地区、性别 | `im_user_profiles`,按隐私设置返回 |
|
| 生日、地区、性别 | `im_user_profiles`,按隐私设置返回 |
|
||||||
|
| 注册手机号 | `im_users.phone`,仅账号安全流程内部使用 |
|
||||||
|
| 邮箱 | `im_user_profiles.email`,仅作为个人资料,不作为认证凭证 |
|
||||||
| 实名姓名、证件号 | 实名表加密存储,不进入 Public |
|
| 实名姓名、证件号 | 实名表加密存储,不进入 Public |
|
||||||
| 群昵称 | `im_group_members`,按需投影到 Subscription Private |
|
| 群昵称 | `im_group_members`,按需投影到 Subscription Private |
|
||||||
|
|
||||||
|
|||||||
+10
-3
@@ -3,10 +3,12 @@
|
|||||||
-- Database: the currently selected tenant database.
|
-- Database: the currently selected tenant database.
|
||||||
-- Important: one tenant owns one database, so these tables intentionally do not contain tenant_id.
|
-- Important: one tenant owns one database, so these tables intentionally do not contain tenant_id.
|
||||||
-- This SQL is for review first. Do not execute directly in production before confirming indexes and migration order.
|
-- This SQL is for review first. Do not execute directly in production before confirming indexes and migration order.
|
||||||
|
-- Non-auto-increment BIGINT primary keys are generated by the server Snowflake generator.
|
||||||
|
|
||||||
-- 1. Group profile and governance truth.
|
-- 1. Group profile and governance truth.
|
||||||
-- Topic/Subscription remains the communication projection; this table is the product business truth.
|
-- Topic/Subscription remains the communication projection; this table is the product business truth.
|
||||||
CREATE TABLE IF NOT EXISTS im_group_settings (
|
CREATE TABLE IF NOT EXISTS im_group_settings (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
topic VARCHAR(32) NOT NULL,
|
topic VARCHAR(32) NOT NULL,
|
||||||
owner_user_id BIGINT NOT NULL,
|
owner_user_id BIGINT NOT NULL,
|
||||||
name VARCHAR(128) NOT NULL DEFAULT '',
|
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
@@ -27,7 +29,8 @@ CREATE TABLE IF NOT EXISTS im_group_settings (
|
|||||||
dissolved_at DATETIME(3),
|
dissolved_at DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (topic),
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_group_settings_topic (topic),
|
||||||
INDEX idx_im_group_settings_owner (owner_user_id, group_status),
|
INDEX idx_im_group_settings_owner (owner_user_id, group_status),
|
||||||
INDEX idx_im_group_settings_status_updated (group_status, updated_at)
|
INDEX idx_im_group_settings_status_updated (group_status, updated_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
@@ -135,19 +138,22 @@ CREATE TABLE IF NOT EXISTS im_group_announcements (
|
|||||||
|
|
||||||
-- 6. Announcement read receipts.
|
-- 6. Announcement read receipts.
|
||||||
CREATE TABLE IF NOT EXISTS im_group_announcement_reads (
|
CREATE TABLE IF NOT EXISTS im_group_announcement_reads (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
announcement_id VARCHAR(64) NOT NULL,
|
announcement_id VARCHAR(64) NOT NULL,
|
||||||
topic VARCHAR(32) NOT NULL,
|
topic VARCHAR(32) NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
read_at DATETIME(3) NOT NULL,
|
read_at DATETIME(3) NOT NULL,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (announcement_id, user_id),
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_group_announcement_reads_announcement_user (announcement_id, user_id),
|
||||||
INDEX idx_im_group_announcement_reads_topic_user (topic, user_id, read_at)
|
INDEX idx_im_group_announcement_reads_topic_user (topic, user_id, read_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
-- 7. Current member mute state.
|
-- 7. Current member mute state.
|
||||||
-- Historical changes are stored in im_group_admin_logs.
|
-- Historical changes are stored in im_group_admin_logs.
|
||||||
CREATE TABLE IF NOT EXISTS im_group_mutes (
|
CREATE TABLE IF NOT EXISTS im_group_mutes (
|
||||||
|
id BIGINT NOT NULL,
|
||||||
topic VARCHAR(32) NOT NULL,
|
topic VARCHAR(32) NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
muted_by BIGINT NOT NULL,
|
muted_by BIGINT NOT NULL,
|
||||||
@@ -160,7 +166,8 @@ CREATE TABLE IF NOT EXISTS im_group_mutes (
|
|||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (topic, user_id),
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE INDEX uk_im_group_mutes_topic_user (topic, user_id),
|
||||||
INDEX idx_im_group_mutes_topic_status (topic, status, expires_at),
|
INDEX idx_im_group_mutes_topic_status (topic, status, expires_at),
|
||||||
INDEX idx_im_group_mutes_user_status (user_id, status, expires_at)
|
INDEX idx_im_group_mutes_user_status (user_id, status, expires_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
> 主要依据:`IM-产品原型需求文档.md`、`IM-前后端对接文档.md`
|
> 主要依据:`IM-产品原型需求文档.md`、`IM-前后端对接文档.md`
|
||||||
> 多租户依据:`IM-独立库多租户改造方案.md`
|
> 多租户依据:`IM-独立库多租户改造方案.md`
|
||||||
> 明确排除:已废弃的 `IM-服务端多租户改造方案.md`
|
> 明确排除:已废弃的 `IM-服务端多租户改造方案.md`
|
||||||
|
> 数据库更新:2026-08-18 已完成无独立 `id` 表的雪花主键改造和本地库迁移。
|
||||||
|
|
||||||
## 1. 审核说明
|
## 1. 审核说明
|
||||||
|
|
||||||
@@ -353,10 +354,13 @@
|
|||||||
- 好友关系、认证安全、隐私、收藏、通知、会议、报表等业务表已在 MySQL 适配器中出现对应存储实现。
|
- 好友关系、认证安全、隐私、收藏、通知、会议、报表等业务表已在 MySQL 适配器中出现对应存储实现。
|
||||||
- 群业务从 Topic/Subscription 扩展为独立业务表,方向符合复杂群管理需求。
|
- 群业务从 Topic/Subscription 扩展为独立业务表,方向符合复杂群管理需求。
|
||||||
- `im_http_idempotency` 和 `im_business_events_outbox` 已纳入应用自动建表与数据库变更汇总脚本。
|
- `im_http_idempotency` 和 `im_business_events_outbox` 已纳入应用自动建表与数据库变更汇总脚本。
|
||||||
|
- MySQL 业务表已统一具备独立 `id` 主键;原先使用自然键或复合主键的 11 张表已改用服务端雪花 `BIGINT id`,原业务键保留唯一索引。
|
||||||
|
- 应用启动会在当前租户库幂等补列、回填存量雪花 ID 并切换主键;迁移使用数据库级 advisory lock,独立租户库与残留 `tenant_id` 旧结构均可保留原业务唯一性。
|
||||||
|
- 用户身份字段已结构化:手机号迁入 `im_users.phone`,邮箱迁入 `im_user_profiles.email`;用户 `tags/im_usertags` 在回填后删除,Topic 标签体系继续保留。
|
||||||
|
|
||||||
### 需要修正
|
### 需要修正
|
||||||
|
|
||||||
1. 建立唯一 schema 版本和迁移入口,禁止“应用启动建部分表 + 手工 SQL 建另一部分表”长期并存。
|
1. 继续把其他历史建表/升级逻辑收敛到唯一 schema 版本和迁移入口;本次雪花主键改造已具备统一自动迁移入口。
|
||||||
2. 群业务 SQL 必须参数化目标租户库,禁止硬编码 `tinode`。
|
2. 群业务 SQL 必须参数化目标租户库,禁止硬编码 `tinode`。
|
||||||
3. 新租户建库和存量租户升级必须执行同一套 migration。
|
3. 新租户建库和存量租户升级必须执行同一套 migration。
|
||||||
4. 为 Topic、Subscription、群业务表之间的复合写操作提供事务边界。
|
4. 为 Topic、Subscription、群业务表之间的复合写操作提供事务边界。
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ func main() {
|
|||||||
INDEX idx_im_friend_requests_inbox(target_id, status, created_at)
|
INDEX idx_im_friend_requests_inbox(target_id, status, created_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_friendships(
|
`CREATE TABLE IF NOT EXISTS tinode.im_friendships(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
peer_id BIGINT NOT NULL,
|
peer_id BIGINT NOT NULL,
|
||||||
status VARCHAR(16) NOT NULL,
|
status VARCHAR(16) NOT NULL,
|
||||||
@@ -139,15 +140,18 @@ func main() {
|
|||||||
topic CHAR(25) NOT NULL DEFAULT '',
|
topic CHAR(25) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id, peer_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_friendships_pair(user_id, peer_id),
|
||||||
INDEX idx_im_friendships_peer(peer_id, status)
|
INDEX idx_im_friendships_peer(peer_id, status)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_user_blocks(
|
`CREATE TABLE IF NOT EXISTS tinode.im_user_blocks(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
blocker_id BIGINT NOT NULL,
|
blocker_id BIGINT NOT NULL,
|
||||||
blocked_id BIGINT NOT NULL,
|
blocked_id BIGINT NOT NULL,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(blocker_id, blocked_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_blocks_pair(blocker_id, blocked_id),
|
||||||
INDEX idx_im_user_blocks_blocked(blocked_id)
|
INDEX idx_im_user_blocks_blocked(blocked_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_favorites(
|
`CREATE TABLE IF NOT EXISTS tinode.im_favorites(
|
||||||
@@ -356,6 +360,7 @@ func main() {
|
|||||||
INDEX idx_im_mailbox_submissions_status(status, id DESC)
|
INDEX idx_im_mailbox_submissions_status(status, id DESC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_user_profiles(
|
`CREATE TABLE IF NOT EXISTS tinode.im_user_profiles(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
@@ -363,13 +368,16 @@ func main() {
|
|||||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_profiles_user(user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings(
|
`CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
add_me_methods JSON,
|
add_me_methods JSON,
|
||||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
@@ -378,7 +386,8 @@ func main() {
|
|||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_privacy_settings_user(user_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_app_releases(
|
`CREATE TABLE IF NOT EXISTS tinode.im_app_releases(
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
@@ -399,6 +408,7 @@ func main() {
|
|||||||
UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel)
|
UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens(
|
`CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
device_id VARCHAR(128) NOT NULL,
|
device_id VARCHAR(128) NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
provider VARCHAR(32) NOT NULL,
|
provider VARCHAR(32) NOT NULL,
|
||||||
@@ -407,11 +417,13 @@ func main() {
|
|||||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(device_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_device_push_tokens_device(device_id),
|
||||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
||||||
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings(
|
`CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
purpose VARCHAR(64) NOT NULL,
|
purpose VARCHAR(64) NOT NULL,
|
||||||
password_hash VARBINARY(255) NOT NULL,
|
password_hash VARBINARY(255) NOT NULL,
|
||||||
@@ -423,7 +435,8 @@ func main() {
|
|||||||
locked_until DATETIME(3),
|
locked_until DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id, purpose)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_youth_mode_settings_user_purpose(user_id, purpose)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges(
|
`CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges(
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
@@ -524,6 +537,8 @@ func main() {
|
|||||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN policy_version INT NOT NULL DEFAULT 1 AFTER enabled",
|
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN policy_version INT NOT NULL DEFAULT 1 AFTER enabled",
|
||||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN restrictions JSON AFTER policy_version",
|
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN restrictions JSON AFTER policy_version",
|
||||||
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions",
|
"ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions",
|
||||||
|
"ALTER TABLE tinode.im_users ADD COLUMN phone VARCHAR(32) NULL AFTER useragent",
|
||||||
|
"ALTER TABLE tinode.im_user_profiles ADD COLUMN email VARCHAR(128) NOT NULL DEFAULT '' AFTER region",
|
||||||
} {
|
} {
|
||||||
if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") {
|
if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") {
|
||||||
fatal(fmt.Errorf("exec failed: %s: %w", stmt, err))
|
fatal(fmt.Errorf("exec failed: %s: %w", stmt, err))
|
||||||
|
|||||||
@@ -203,18 +203,17 @@ func (a *authenticator) UpdateRecord(ctx auth.AuthContext, rec *auth.Rec, secret
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old tag from the list of tags
|
if a.addToTags {
|
||||||
oldTag := a.name + ":" + login
|
oldTag := a.name + ":" + login
|
||||||
for i, tag := range rec.Tags {
|
for i, tag := range rec.Tags {
|
||||||
if tag == oldTag {
|
if tag == oldTag {
|
||||||
rec.Tags[i] = rec.Tags[len(rec.Tags)-1]
|
rec.Tags[i] = rec.Tags[len(rec.Tags)-1]
|
||||||
rec.Tags = rec.Tags[:len(rec.Tags)-1]
|
rec.Tags = rec.Tags[:len(rec.Tags)-1]
|
||||||
|
break
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
|
rec.Tags = append(rec.Tags, a.name+":"+uname)
|
||||||
}
|
}
|
||||||
// Add new tag
|
|
||||||
rec.Tags = append(rec.Tags, a.name+":"+uname)
|
|
||||||
|
|
||||||
return rec, nil
|
return rec, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-151
@@ -246,6 +246,12 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
|
|||||||
if err = ensureIMHTTPInfrastructureTables(a.db); err != nil {
|
if err = ensureIMHTTPInfrastructureTables(a.db); err != nil {
|
||||||
return errors.New("mysql adapter failed to ensure HTTP infrastructure schema: " + err.Error())
|
return errors.New("mysql adapter failed to ensure HTTP infrastructure schema: " + err.Error())
|
||||||
}
|
}
|
||||||
|
if err = ensureSnowflakePrimaryKeys(a.db); err != nil {
|
||||||
|
return errors.New("mysql adapter failed to ensure snowflake primary keys: " + err.Error())
|
||||||
|
}
|
||||||
|
if err = ensureUserIdentitySchema(a.db); err != nil {
|
||||||
|
return errors.New("mysql adapter failed to ensure user identity schema: " + err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -440,6 +446,14 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
|
|||||||
db.Close()
|
db.Close()
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
if err = ensureSnowflakePrimaryKeys(db); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err = ensureUserIdentitySchema(db); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
tenantAdp := &adapter{
|
tenantAdp := &adapter{
|
||||||
db: db,
|
db: db,
|
||||||
dsn: mysqlDefaultString(dsn, a.dsn),
|
dsn: mysqlDefaultString(dsn, a.dsn),
|
||||||
@@ -589,7 +603,13 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
if _, err = tx.Exec("DROP DATABASE IF EXISTS " + quoteIdent(businessDB)); err != nil {
|
if _, err = tx.Exec("DROP DATABASE IF EXISTS " + quoteIdent(businessDB)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err = tx.Exec("DROP DATABASE IF EXISTS " + quoteIdent(registryDB)); err != nil {
|
if _, err = tx.Exec("CREATE DATABASE IF NOT EXISTS " + quoteIdent(registryDB) + " CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec("DROP TABLE IF EXISTS " + quoteIdent(registryDB) + ".im_tenant_config"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec("DROP TABLE IF EXISTS " + quoteIdent(registryDB) + ".im_tenant"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,10 +636,11 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
access JSON,
|
access JSON,
|
||||||
lastseen DATETIME,
|
lastseen DATETIME,
|
||||||
useragent VARCHAR(255) DEFAULT '',
|
useragent VARCHAR(255) DEFAULT '',
|
||||||
|
phone VARCHAR(32),
|
||||||
public JSON,
|
public JSON,
|
||||||
trusted JSON,
|
trusted JSON,
|
||||||
tags JSON,
|
|
||||||
PRIMARY KEY(id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX users_phone(phone),
|
||||||
INDEX users_state_stateat(state, stateat),
|
INDEX users_state_stateat(state, stateat),
|
||||||
INDEX users_lastseen_updatedat(lastseen, updatedat)
|
INDEX users_lastseen_updatedat(lastseen, updatedat)
|
||||||
)`); err != nil {
|
)`); err != nil {
|
||||||
@@ -628,12 +649,14 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
|
|
||||||
if _, err = tx.Exec(
|
if _, err = tx.Exec(
|
||||||
`CREATE TABLE im_user_security(
|
`CREATE TABLE im_user_security(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
auth_version INT NOT NULL DEFAULT 1,
|
auth_version INT NOT NULL DEFAULT 1,
|
||||||
password_updated_at DATETIME(3),
|
password_updated_at DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_security_user(user_id)
|
||||||
)`); err != nil {
|
)`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -645,20 +668,6 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Indexed user tags.
|
|
||||||
if _, err = tx.Exec(
|
|
||||||
`CREATE TABLE im_usertags(
|
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
|
||||||
userid BIGINT NOT NULL,
|
|
||||||
tag VARCHAR(96) NOT NULL,
|
|
||||||
PRIMARY KEY(id),
|
|
||||||
FOREIGN KEY(userid) REFERENCES im_users(id),
|
|
||||||
INDEX usertags_tag(tag),
|
|
||||||
UNIQUE INDEX usertags_userid_tag(userid, tag)
|
|
||||||
)`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indexed devices. Normalized into a separate table.
|
// Indexed devices. Normalized into a separate table.
|
||||||
if _, err = tx.Exec(
|
if _, err = tx.Exec(
|
||||||
`CREATE TABLE im_devices(
|
`CREATE TABLE im_devices(
|
||||||
@@ -919,15 +928,21 @@ func (a *adapter) CreateDb(reset bool) error {
|
|||||||
|
|
||||||
if _, err = tx.Exec(
|
if _, err = tx.Exec(
|
||||||
`CREATE TABLE im_kvmeta(` +
|
`CREATE TABLE im_kvmeta(` +
|
||||||
|
"id BIGINT NOT NULL," +
|
||||||
"`key` VARCHAR(64) NOT NULL," +
|
"`key` VARCHAR(64) NOT NULL," +
|
||||||
"createdat DATETIME(3)," +
|
"createdat DATETIME(3)," +
|
||||||
"`value` TEXT," +
|
"`value` TEXT," +
|
||||||
"PRIMARY KEY(`key`)," +
|
"PRIMARY KEY(id)," +
|
||||||
|
"UNIQUE INDEX uk_im_kvmeta_key(`key`)," +
|
||||||
"INDEX kvmeta_createdat_key(createdat, `key`)" +
|
"INDEX kvmeta_createdat_key(createdat, `key`)" +
|
||||||
`)`); err != nil {
|
`)`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err = tx.Exec("INSERT INTO im_kvmeta(`key`, `value`) VALUES('version',?)", adpVersion); err != nil {
|
versionID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec("INSERT INTO im_kvmeta(id, `key`, `value`) VALUES(?,'version',?)", versionID, adpVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1225,6 +1240,7 @@ func createIMMailboxTables(tx *sql.Tx) error {
|
|||||||
func createIMUserProductTables(tx *sql.Tx) error {
|
func createIMUserProductTables(tx *sql.Tx) error {
|
||||||
statements := []string{
|
statements := []string{
|
||||||
`CREATE TABLE im_user_profiles(
|
`CREATE TABLE im_user_profiles(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
@@ -1232,13 +1248,16 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
|||||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_profiles_user(user_id)
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE im_user_privacy_settings(
|
`CREATE TABLE im_user_privacy_settings(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
add_me_methods JSON,
|
add_me_methods JSON,
|
||||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||||
@@ -1247,7 +1266,8 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
|||||||
version INT NOT NULL DEFAULT 1,
|
version INT NOT NULL DEFAULT 1,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_privacy_settings_user(user_id)
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE im_app_releases(
|
`CREATE TABLE im_app_releases(
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
@@ -1268,6 +1288,7 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
|||||||
UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel)
|
UNIQUE INDEX uk_im_app_releases_platform_channel(platform, channel)
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE im_device_push_tokens(
|
`CREATE TABLE im_device_push_tokens(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
device_id VARCHAR(128) NOT NULL,
|
device_id VARCHAR(128) NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
provider VARCHAR(32) NOT NULL,
|
provider VARCHAR(32) NOT NULL,
|
||||||
@@ -1276,7 +1297,8 @@ func createIMUserProductTables(tx *sql.Tx) error {
|
|||||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(device_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_device_push_tokens_device(device_id),
|
||||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
UNIQUE INDEX uk_im_device_push_tokens_provider_token(provider, token),
|
||||||
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
INDEX idx_im_device_push_tokens_user(user_id, updated_at)
|
||||||
)`,
|
)`,
|
||||||
@@ -1443,6 +1465,7 @@ func createIMRelationTables(exec sqlExecutor) error {
|
|||||||
INDEX idx_im_friend_requests_inbox(target_id, status, created_at)
|
INDEX idx_im_friend_requests_inbox(target_id, status, created_at)
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE im_friendships(
|
`CREATE TABLE im_friendships(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
peer_id BIGINT NOT NULL,
|
peer_id BIGINT NOT NULL,
|
||||||
status VARCHAR(16) NOT NULL,
|
status VARCHAR(16) NOT NULL,
|
||||||
@@ -1450,15 +1473,18 @@ func createIMRelationTables(exec sqlExecutor) error {
|
|||||||
topic CHAR(25) NOT NULL DEFAULT '',
|
topic CHAR(25) NOT NULL DEFAULT '',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id, peer_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_friendships_pair(user_id, peer_id),
|
||||||
INDEX idx_im_friendships_peer(peer_id, status)
|
INDEX idx_im_friendships_peer(peer_id, status)
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE im_user_blocks(
|
`CREATE TABLE im_user_blocks(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
blocker_id BIGINT NOT NULL,
|
blocker_id BIGINT NOT NULL,
|
||||||
blocked_id BIGINT NOT NULL,
|
blocked_id BIGINT NOT NULL,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(blocker_id, blocked_id),
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_user_blocks_pair(blocker_id, blocked_id),
|
||||||
INDEX idx_im_user_blocks_blocked(blocked_id)
|
INDEX idx_im_user_blocks_blocked(blocked_id)
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE im_favorites(
|
`CREATE TABLE im_favorites(
|
||||||
@@ -1536,6 +1562,7 @@ func createIMSecurityTables(tx *sql.Tx) error {
|
|||||||
}
|
}
|
||||||
_, err := tx.Exec(
|
_, err := tx.Exec(
|
||||||
`CREATE TABLE im_youth_mode_settings(
|
`CREATE TABLE im_youth_mode_settings(
|
||||||
|
id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
purpose VARCHAR(64) NOT NULL,
|
purpose VARCHAR(64) NOT NULL,
|
||||||
password_hash VARBINARY(255) NOT NULL,
|
password_hash VARBINARY(255) NOT NULL,
|
||||||
@@ -1547,7 +1574,8 @@ func createIMSecurityTables(tx *sql.Tx) error {
|
|||||||
locked_until DATETIME(3),
|
locked_until DATETIME(3),
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(user_id, purpose)
|
PRIMARY KEY(id),
|
||||||
|
UNIQUE INDEX uk_im_youth_mode_settings_user_purpose(user_id, purpose)
|
||||||
)`)
|
)`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1650,27 +1678,31 @@ func (a *adapter) UserCreate(user *t.User) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
decoded_uid := store.DecodeUid(user.Uid())
|
decoded_uid := store.DecodeUid(user.Uid())
|
||||||
if _, err = tx.Exec("INSERT INTO im_users(id,createdat,updatedat,state,access,public,trusted,tags) VALUES(?,?,?,?,?,?,?,?)",
|
securityID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var phone any
|
||||||
|
if strings.TrimSpace(user.Phone) != "" {
|
||||||
|
phone = strings.TrimSpace(user.Phone)
|
||||||
|
}
|
||||||
|
if _, err = tx.Exec("INSERT INTO im_users(id,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?)",
|
||||||
decoded_uid,
|
decoded_uid,
|
||||||
user.CreatedAt,
|
user.CreatedAt,
|
||||||
user.UpdatedAt,
|
user.UpdatedAt,
|
||||||
user.State,
|
user.State,
|
||||||
user.Access,
|
user.Access,
|
||||||
|
phone,
|
||||||
common.ToJSON(user.Public),
|
common.ToJSON(user.Public),
|
||||||
common.ToJSON(user.Trusted),
|
common.ToJSON(user.Trusted)); err != nil {
|
||||||
user.Tags); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = tx.Exec("INSERT INTO im_user_security(user_id,auth_version,created_at,updated_at) VALUES(?,?,?,?)",
|
if _, err = tx.Exec("INSERT INTO im_user_security(id,user_id,auth_version,created_at,updated_at) VALUES(?,?,?,?,?)",
|
||||||
decoded_uid, 1, user.CreatedAt, user.UpdatedAt); err != nil {
|
securityID, decoded_uid, 1, user.CreatedAt, user.UpdatedAt); err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save user's tags to a separate table to make user findable.
|
|
||||||
if err = addTags(tx, "im_usertags", "userid", decoded_uid, user.Tags, false); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
user.Tags = nil
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
@@ -1895,7 +1927,8 @@ func (a *adapter) UserGet(uid t.Uid) (*t.User, error) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var user t.User
|
var user t.User
|
||||||
err := a.db.GetContext(ctx, &user, "SELECT * FROM im_users WHERE id=? AND state!=?", store.DecodeUid(uid), t.StateDeleted)
|
err := a.db.GetContext(ctx, &user, `SELECT id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||||
|
COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE id=? AND state!=?`, store.DecodeUid(uid), t.StateDeleted)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
user.SetUid(uid)
|
user.SetUid(uid)
|
||||||
user.Public = common.FromJSON(user.Public)
|
user.Public = common.FromJSON(user.Public)
|
||||||
@@ -1925,7 +1958,8 @@ func (a *adapter) UserGetAll(ids ...t.Uid) ([]t.User, error) {
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
q, uids, _ := sqlx.In("SELECT * FROM im_users WHERE id IN (?) AND state!=?", uids, t.StateDeleted)
|
q, uids, _ := sqlx.In(`SELECT id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||||
|
COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE id IN (?) AND state!=?`, uids, t.StateDeleted)
|
||||||
rows, err := a.db.QueryxContext(ctx, a.db.Rebind(q), uids...)
|
rows, err := a.db.QueryxContext(ctx, a.db.Rebind(q), uids...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2048,10 +2082,6 @@ func (a *adapter) UserDelete(uid t.Uid, hard bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = tx.Exec("DELETE FROM im_usertags WHERE userid=?", decoded_uid); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = tx.Exec("DELETE FROM im_users WHERE id=?", decoded_uid); err != nil {
|
if _, err = tx.Exec("DELETE FROM im_users WHERE id=?", decoded_uid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2150,7 +2180,11 @@ func (a *adapter) UserUpdate(uid t.Uid, update map[string]any) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
update = withoutUserTags(update)
|
||||||
cols, args := common.UpdateByMap(update)
|
cols, args := common.UpdateByMap(update)
|
||||||
|
if len(cols) == 0 {
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
decoded_uid := store.DecodeUid(uid)
|
decoded_uid := store.DecodeUid(uid)
|
||||||
args = append(args, decoded_uid)
|
args = append(args, decoded_uid)
|
||||||
_, err = tx.Exec("UPDATE im_users SET "+strings.Join(cols, ",")+" WHERE id=?", args...)
|
_, err = tx.Exec("UPDATE im_users SET "+strings.Join(cols, ",")+" WHERE id=?", args...)
|
||||||
@@ -2166,76 +2200,12 @@ func (a *adapter) UserUpdate(uid t.Uid, update map[string]any) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags are also stored in a separate table
|
|
||||||
if tags := common.ExtractTags(update); tags != nil {
|
|
||||||
// First delete all user tags
|
|
||||||
_, err = tx.Exec("DELETE FROM im_usertags WHERE userid=?", decoded_uid)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Now insert new tags
|
|
||||||
err = addTags(tx, "im_usertags", "userid", decoded_uid, tags, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserUpdateTags adds, removes, or resets user's tags.
|
// UserUpdateTags is retained for adapter compatibility; user tags are disabled.
|
||||||
func (a *adapter) UserUpdateTags(uid t.Uid, add, remove, reset []string) ([]string, error) {
|
func (a *adapter) UserUpdateTags(uid t.Uid, add, remove, reset []string) ([]string, error) {
|
||||||
ctx, cancel := a.getContextForTx()
|
return nil, nil
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
tx, err := a.db.BeginTxx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
decoded_uid := store.DecodeUid(uid)
|
|
||||||
|
|
||||||
if reset != nil {
|
|
||||||
// Delete all tags first if resetting.
|
|
||||||
_, err = tx.Exec("DELETE FROM im_usertags WHERE userid=?", decoded_uid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
add = reset
|
|
||||||
remove = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now insert new tags. Ignore duplicates if resetting.
|
|
||||||
err = addTags(tx, "im_usertags", "userid", decoded_uid, add, reset == nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete tags.
|
|
||||||
err = removeTags(tx, "im_usertags", "userid", decoded_uid, remove)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var allTags []string
|
|
||||||
err = tx.Select(&allTags, "SELECT tag FROM im_usertags WHERE userid=?", decoded_uid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("UPDATE im_users SET tags=? WHERE id=?", t.StringSlice(allTags), decoded_uid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return allTags, tx.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserGetByCred returns user ID for the given validated credential.
|
// UserGetByCred returns user ID for the given validated credential.
|
||||||
@@ -2245,7 +2215,12 @@ func (a *adapter) UserGetByCred(method, value string) (t.Uid, error) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var decoded_uid int64
|
var decoded_uid int64
|
||||||
err := a.db.GetContext(ctx, &decoded_uid, "SELECT userid FROM im_credentials WHERE synthetic=?", method+":"+value)
|
var err error
|
||||||
|
if method == "tel" {
|
||||||
|
err = a.db.GetContext(ctx, &decoded_uid, "SELECT id FROM im_users WHERE phone=? AND state!=?", value, t.StateDeleted)
|
||||||
|
} else {
|
||||||
|
err = a.db.GetContext(ctx, &decoded_uid, "SELECT userid FROM im_credentials WHERE synthetic=?", method+":"+value)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return store.EncodeUid(decoded_uid), nil
|
return store.EncodeUid(decoded_uid), nil
|
||||||
}
|
}
|
||||||
@@ -2266,6 +2241,22 @@ func (a *adapter) TenantUserGetByCred(tenantID t.TenantID, method, value string)
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.UserGetByCred(method, value)
|
return tenantAdp.UserGetByCred(method, value)
|
||||||
}
|
}
|
||||||
|
if !a.tenantScopedDB && method == "tel" {
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
var decodedUID int64
|
||||||
|
err = a.db.GetContext(ctx, &decodedUID,
|
||||||
|
"SELECT id FROM im_users WHERE tenant_id=? AND phone=? AND state!=?", tenantID, value, t.StateDeleted)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return t.ZeroUid, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return t.ZeroUid, err
|
||||||
|
}
|
||||||
|
return store.EncodeUid(decodedUID), nil
|
||||||
|
}
|
||||||
return a.UserGetByCred(method, value)
|
return a.UserGetByCred(method, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3329,17 +3320,21 @@ func (a *adapter) SubsDelForUser(user t.Uid, hard bool) error {
|
|||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find returns a list of users or group topics who match given tags, such as "email:jdoe@example.com" or "tel:+18003287448".
|
// Find returns group topics which match the requested discovery tags.
|
||||||
func (a *adapter) Find(caller, promoPrefix string, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
|
func (a *adapter) Find(caller, promoPrefix string, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
|
||||||
|
index := make(map[string]struct{})
|
||||||
|
allReq := t.FlattenDoubleSlice(req)
|
||||||
|
allTags := append(append([]string{}, allReq...), opt...)
|
||||||
|
if len(allTags) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
var args []any
|
var args []any
|
||||||
stateConstraint := ""
|
stateConstraint := ""
|
||||||
if activeOnly {
|
if activeOnly {
|
||||||
args = append(args, t.StateOK)
|
args = append(args, t.StateOK)
|
||||||
stateConstraint = "u.state=? AND "
|
stateConstraint = "t.state=? AND "
|
||||||
}
|
}
|
||||||
index := make(map[string]struct{})
|
for _, tag := range allTags {
|
||||||
allReq := t.FlattenDoubleSlice(req)
|
|
||||||
for _, tag := range append(allReq, opt...) {
|
|
||||||
args = append(args, tag)
|
args = append(args, tag)
|
||||||
index[tag] = struct{}{}
|
index[tag] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -3352,29 +3347,9 @@ func (a *adapter) Find(caller, promoPrefix string, req [][]string, opt []string,
|
|||||||
matcher = "COUNT(*)"
|
matcher = "COUNT(*)"
|
||||||
}
|
}
|
||||||
|
|
||||||
query := "SELECT u.id,u.createdat,u.updatedat,0,u.access,0 AS subcnt,u.public,u.trusted,u.tags," + matcher + " AS matches " +
|
query := "SELECT t.name AS topic,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags," + matcher + " AS matches " +
|
||||||
"FROM im_users AS u JOIN im_usertags AS tg ON tg.userid=u.id " +
|
|
||||||
"WHERE " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allReq)+len(opt)-1) + ") " +
|
|
||||||
"GROUP BY u.id,u.createdat,u.updatedat,u.access,u.public,u.trusted,u.tags "
|
|
||||||
if len(allReq) > 0 {
|
|
||||||
q, a := common.DisjunctionSql(req, "tg.tag")
|
|
||||||
query += q
|
|
||||||
args = append(args, a...)
|
|
||||||
}
|
|
||||||
|
|
||||||
query += "UNION ALL "
|
|
||||||
|
|
||||||
if activeOnly {
|
|
||||||
args = append(args, t.StateOK)
|
|
||||||
stateConstraint = "t.state=? AND "
|
|
||||||
}
|
|
||||||
for _, tag := range append(allReq, opt...) {
|
|
||||||
args = append(args, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
query += "SELECT t.name AS topic,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags," + matcher + " AS matches " +
|
|
||||||
"FROM im_topics AS t JOIN im_topictags AS tg ON t.name=tg.topic " +
|
"FROM im_topics AS t JOIN im_topictags AS tg ON t.name=tg.topic " +
|
||||||
"WHERE " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allReq)+len(opt)-1) + ") " +
|
"WHERE " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allTags)-1) + ") " +
|
||||||
"GROUP BY t.name,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags "
|
"GROUP BY t.name,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags "
|
||||||
if len(allReq) > 0 {
|
if len(allReq) > 0 {
|
||||||
q, a := common.DisjunctionSql(req, "tg.tag")
|
q, a := common.DisjunctionSql(req, "tg.tag")
|
||||||
@@ -3443,29 +3418,17 @@ func (a *adapter) Find(caller, promoPrefix string, req [][]string, opt []string,
|
|||||||
return subs, err
|
return subs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindOne returns the first topic or user which matches the given tag.
|
// FindOne returns the first topic which matches the given tag.
|
||||||
func (a *adapter) FindOne(tag string) (string, error) {
|
func (a *adapter) FindOne(tag string) (string, error) {
|
||||||
var args []any
|
|
||||||
|
|
||||||
query := "SELECT t.name AS topic FROM im_topics AS t LEFT JOIN im_topictags AS tt ON t.name=tt.topic " +
|
query := "SELECT t.name AS topic FROM im_topics AS t LEFT JOIN im_topictags AS tt ON t.name=tt.topic " +
|
||||||
"WHERE tt.tag=?"
|
"WHERE tt.tag=? LIMIT 1"
|
||||||
args = append(args, tag)
|
|
||||||
|
|
||||||
query += " UNION ALL "
|
|
||||||
|
|
||||||
query += "SELECT u.id AS topic FROM im_users AS u LEFT JOIN im_usertags AS ut ON ut.userid=u.id " +
|
|
||||||
"WHERE ut.tag=?"
|
|
||||||
args = append(args, tag)
|
|
||||||
|
|
||||||
// LIMIT is applied to all resultant rows.
|
|
||||||
query += " LIMIT 1"
|
|
||||||
|
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := a.db.QueryxContext(ctx, query, args...)
|
rows, err := a.db.QueryxContext(ctx, query, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -3998,6 +3961,11 @@ func (a *adapter) CredUpsert(cred *t.Credential) (bool, error) {
|
|||||||
}
|
}
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
if cred.Done && cred.Method == "tel" {
|
||||||
|
if _, err = tx.Exec("UPDATE im_users SET phone=? WHERE id=? AND (phone IS NULL OR phone='')", cred.Value, userId); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return true, tx.Commit()
|
return true, tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4635,7 +4603,11 @@ func (a *adapter) PCacheUpsert(key string, value string, failOnDuplicate bool) e
|
|||||||
action = "REPLACE"
|
action = "REPLACE"
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := a.db.ExecContext(ctx, action+" INTO im_kvmeta(`key`,createdat,`value`) VALUES(?,?,?)", key, t.TimeNow(), value)
|
id, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx, action+" INTO im_kvmeta(id,`key`,createdat,`value`) VALUES(?,?,?,?)", id, key, t.TimeNow(), value)
|
||||||
if isDupe(err) {
|
if isDupe(err) {
|
||||||
return t.ErrDuplicate
|
return t.ErrDuplicate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package mysql
|
|||||||
func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
||||||
statements := []string{
|
statements := []string{
|
||||||
`CREATE TABLE IF NOT EXISTS im_group_settings (
|
`CREATE TABLE IF NOT EXISTS im_group_settings (
|
||||||
topic VARCHAR(32) NOT NULL, owner_user_id BIGINT NOT NULL,
|
id BIGINT NOT NULL, topic VARCHAR(32) NOT NULL, owner_user_id BIGINT NOT NULL,
|
||||||
name VARCHAR(128) NOT NULL DEFAULT '', avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
name VARCHAR(128) NOT NULL DEFAULT '', avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
description VARCHAR(512) NOT NULL DEFAULT '', group_status VARCHAR(16) NOT NULL DEFAULT 'active',
|
description VARCHAR(512) NOT NULL DEFAULT '', group_status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||||
join_approval TINYINT NOT NULL DEFAULT 0, mute_all TINYINT NOT NULL DEFAULT 0,
|
join_approval TINYINT NOT NULL DEFAULT 0, mute_all TINYINT NOT NULL DEFAULT 0,
|
||||||
@@ -19,7 +19,8 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
|||||||
version INT NOT NULL DEFAULT 1, created_by BIGINT NOT NULL, dissolved_by BIGINT,
|
version INT NOT NULL DEFAULT 1, created_by BIGINT NOT NULL, dissolved_by BIGINT,
|
||||||
dissolved_at DATETIME(3), created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
dissolved_at DATETIME(3), created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(topic), INDEX idx_im_group_settings_owner(owner_user_id, group_status),
|
PRIMARY KEY(id), UNIQUE INDEX uk_im_group_settings_topic(topic),
|
||||||
|
INDEX idx_im_group_settings_owner(owner_user_id, group_status),
|
||||||
INDEX idx_im_group_settings_status_updated(group_status, updated_at)
|
INDEX idx_im_group_settings_status_updated(group_status, updated_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS im_group_members (
|
`CREATE TABLE IF NOT EXISTS im_group_members (
|
||||||
@@ -77,18 +78,20 @@ func ensureIMGroupBusinessTables(exec sqlExecutor) error {
|
|||||||
INDEX idx_im_group_announcements_topic_time(topic, published_at DESC, id DESC)
|
INDEX idx_im_group_announcements_topic_time(topic, published_at DESC, id DESC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS im_group_announcement_reads (
|
`CREATE TABLE IF NOT EXISTS im_group_announcement_reads (
|
||||||
announcement_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL,
|
id BIGINT NOT NULL, announcement_id VARCHAR(64) NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL,
|
||||||
read_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
read_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(announcement_id, user_id), INDEX idx_im_group_announcement_reads_topic_user(topic, user_id, read_at)
|
PRIMARY KEY(id), UNIQUE INDEX uk_im_group_announcement_reads_announcement_user(announcement_id, user_id),
|
||||||
|
INDEX idx_im_group_announcement_reads_topic_user(topic, user_id, read_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS im_group_mutes (
|
`CREATE TABLE IF NOT EXISTS im_group_mutes (
|
||||||
topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, muted_by BIGINT NOT NULL,
|
id BIGINT NOT NULL, topic VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, muted_by BIGINT NOT NULL,
|
||||||
muted_at DATETIME(3) NOT NULL, expires_at DATETIME(3), reason VARCHAR(256) NOT NULL DEFAULT '',
|
muted_at DATETIME(3) NOT NULL, expires_at DATETIME(3), reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active', revoked_by BIGINT, revoked_at DATETIME(3),
|
status VARCHAR(16) NOT NULL DEFAULT 'active', revoked_by BIGINT, revoked_at DATETIME(3),
|
||||||
version INT NOT NULL DEFAULT 1, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
version INT NOT NULL DEFAULT 1, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(topic, user_id), INDEX idx_im_group_mutes_topic_status(topic, status, expires_at),
|
PRIMARY KEY(id), UNIQUE INDEX uk_im_group_mutes_topic_user(topic, user_id),
|
||||||
|
INDEX idx_im_group_mutes_topic_status(topic, status, expires_at),
|
||||||
INDEX idx_im_group_mutes_user_status(user_id, status, expires_at)
|
INDEX idx_im_group_mutes_user_status(user_id, status, expires_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`,
|
||||||
`CREATE TABLE IF NOT EXISTS im_group_qrcodes (
|
`CREATE TABLE IF NOT EXISTS im_group_qrcodes (
|
||||||
|
|||||||
@@ -30,4 +30,14 @@ func TestEnsureIMGroupBusinessTablesUsesSelectedTenantDatabase(t *testing.T) {
|
|||||||
t.Fatalf("group schema must use the selected tenant database: %s", statement)
|
t.Fatalf("group schema must use the selected tenant database: %s", statement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
joined := strings.ToLower(strings.Join(exec.statements, "\n"))
|
||||||
|
for _, expected := range []string{
|
||||||
|
"primary key(id), unique index uk_im_group_settings_topic(topic)",
|
||||||
|
"primary key(id), unique index uk_im_group_announcement_reads_announcement_user(announcement_id, user_id)",
|
||||||
|
"primary key(id), unique index uk_im_group_mutes_topic_user(topic, user_id)",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(joined, expected) {
|
||||||
|
t.Fatalf("group schema is missing snowflake primary-key definition %q", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
//go:build mysql
|
||||||
|
// +build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/tinode/chat/server/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type snowflakePrimaryKeySpec struct {
|
||||||
|
table string
|
||||||
|
uniqueIndex string
|
||||||
|
businessKey []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var snowflakePrimaryKeySpecs = []snowflakePrimaryKeySpec{
|
||||||
|
{table: "im_user_security", uniqueIndex: "uk_im_user_security_user", businessKey: []string{"user_id"}},
|
||||||
|
{table: "im_kvmeta", uniqueIndex: "uk_im_kvmeta_key", businessKey: []string{"key"}},
|
||||||
|
{table: "im_user_profiles", uniqueIndex: "uk_im_user_profiles_user", businessKey: []string{"user_id"}},
|
||||||
|
{table: "im_user_privacy_settings", uniqueIndex: "uk_im_user_privacy_settings_user", businessKey: []string{"user_id"}},
|
||||||
|
{table: "im_device_push_tokens", uniqueIndex: "uk_im_device_push_tokens_device", businessKey: []string{"device_id"}},
|
||||||
|
{table: "im_friendships", uniqueIndex: "uk_im_friendships_pair", businessKey: []string{"user_id", "peer_id"}},
|
||||||
|
{table: "im_user_blocks", uniqueIndex: "uk_im_user_blocks_pair", businessKey: []string{"blocker_id", "blocked_id"}},
|
||||||
|
{table: "im_youth_mode_settings", uniqueIndex: "uk_im_youth_mode_settings_user_purpose", businessKey: []string{"user_id", "purpose"}},
|
||||||
|
{table: "im_group_settings", uniqueIndex: "uk_im_group_settings_topic", businessKey: []string{"topic"}},
|
||||||
|
{table: "im_group_announcement_reads", uniqueIndex: "uk_im_group_announcement_reads_announcement_user", businessKey: []string{"announcement_id", "user_id"}},
|
||||||
|
{table: "im_group_mutes", uniqueIndex: "uk_im_group_mutes_topic_user", businessKey: []string{"topic", "user_id"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
const snowflakeMigrationLock = "CONCAT('im_snowflake_pk:', LEFT(SHA2(DATABASE(), 256), 40))"
|
||||||
|
|
||||||
|
func nextSnowflakeID() (int64, error) {
|
||||||
|
uid := store.Store.GetUid()
|
||||||
|
if uid.IsZero() {
|
||||||
|
return 0, errors.New("mysql: snowflake generator is not initialized")
|
||||||
|
}
|
||||||
|
id := store.DecodeUid(uid)
|
||||||
|
if id <= 0 {
|
||||||
|
return 0, errors.New("mysql: snowflake generator returned an invalid ID")
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSnowflakePrimaryKeys upgrades legacy business-key primary keys in the
|
||||||
|
// selected tenant database. The former primary-key columns remain unique.
|
||||||
|
func ensureSnowflakePrimaryKeys(db *sqlx.DB) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
conn, err := db.Connx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
var locked sql.NullInt64
|
||||||
|
if err = conn.QueryRowContext(ctx,
|
||||||
|
"SELECT GET_LOCK("+snowflakeMigrationLock+", 60)").Scan(&locked); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !locked.Valid || locked.Int64 != 1 {
|
||||||
|
return errors.New("mysql: timed out waiting for snowflake primary-key migration lock")
|
||||||
|
}
|
||||||
|
defer conn.ExecContext(ctx, "SELECT RELEASE_LOCK("+snowflakeMigrationLock+")")
|
||||||
|
|
||||||
|
for _, spec := range snowflakePrimaryKeySpecs {
|
||||||
|
if err = migrateSnowflakePrimaryKey(ctx, conn, spec); err != nil {
|
||||||
|
return fmt.Errorf("mysql: migrate %s primary key: %w", spec.table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateSnowflakePrimaryKey(ctx context.Context, conn *sqlx.Conn, spec snowflakePrimaryKeySpec) error {
|
||||||
|
var exists int
|
||||||
|
if err := conn.QueryRowContext(ctx, `SELECT EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?)`, spec.table).Scan(&exists); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.QueryRowContext(ctx, `SELECT EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name='id')`, spec.table).Scan(&exists); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists == 0 {
|
||||||
|
if _, err := conn.ExecContext(ctx, "ALTER TABLE "+quoteIdent(spec.table)+" ADD COLUMN id BIGINT NULL FIRST"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := conn.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM "+quoteIdent(spec.table)+" WHERE id IS NULL OR id=0 LIMIT 1)").Scan(&exists); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
id, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := conn.ExecContext(ctx, "UPDATE "+quoteIdent(spec.table)+" SET id=? WHERE id IS NULL OR id=0 LIMIT 1", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected, err := result.RowsAffected(); err != nil || affected != 1 {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errors.New("could not backfill exactly one row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.QueryRowContext(ctx, `SELECT EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND index_name=?)`,
|
||||||
|
spec.table, spec.uniqueIndex).Scan(&exists); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists == 0 {
|
||||||
|
businessKey := spec.businessKey
|
||||||
|
var hasTenantID int
|
||||||
|
if err := conn.QueryRowContext(ctx, `SELECT EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name='tenant_id')`,
|
||||||
|
spec.table).Scan(&hasTenantID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasTenantID == 1 {
|
||||||
|
businessKey = append([]string{"tenant_id"}, businessKey...)
|
||||||
|
}
|
||||||
|
columns := make([]string, len(businessKey))
|
||||||
|
for i, column := range businessKey {
|
||||||
|
columns[i] = quoteIdent(column)
|
||||||
|
}
|
||||||
|
statement := "CREATE UNIQUE INDEX " + quoteIdent(spec.uniqueIndex) + " ON " +
|
||||||
|
quoteIdent(spec.table) + "(" + strings.Join(columns, ",") + ")"
|
||||||
|
if _, err := conn.ExecContext(ctx, statement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var primaryColumns sql.NullString
|
||||||
|
if err := conn.QueryRowContext(ctx, `SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR ',')
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema=DATABASE() AND table_name=? AND index_name='PRIMARY'`, spec.table).Scan(&primaryColumns); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if primaryColumns.Valid && primaryColumns.String == "id" {
|
||||||
|
var dataType, nullable, extra string
|
||||||
|
if err := conn.QueryRowContext(ctx, `SELECT data_type,is_nullable,extra
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name=? AND column_name='id'`, spec.table).
|
||||||
|
Scan(&dataType, &nullable, &extra); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.EqualFold(dataType, "bigint") && strings.EqualFold(nullable, "NO") &&
|
||||||
|
!strings.Contains(strings.ToLower(extra), "auto_increment") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := conn.ExecContext(ctx, "ALTER TABLE "+quoteIdent(spec.table)+" MODIFY COLUMN id BIGINT NOT NULL")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
statement := "ALTER TABLE " + quoteIdent(spec.table) + " MODIFY COLUMN id BIGINT NOT NULL"
|
||||||
|
if primaryColumns.Valid && primaryColumns.String != "" {
|
||||||
|
statement += ", DROP PRIMARY KEY"
|
||||||
|
}
|
||||||
|
statement += ", ADD PRIMARY KEY(id)"
|
||||||
|
_, err := conn.ExecContext(ctx, statement)
|
||||||
|
return err
|
||||||
|
}
|
||||||
+116
-123
@@ -152,7 +152,8 @@ func (a *adapter) TenantUserGet(id t.TenantID, uid t.Uid) (*t.User, error) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
var user t.User
|
var user t.User
|
||||||
err = a.db.GetContext(ctx, &user, "SELECT * FROM im_users WHERE tenant_id=? AND id=? AND state!=?",
|
err = a.db.GetContext(ctx, &user, `SELECT tenant_id,id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||||
|
COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE tenant_id=? AND id=? AND state!=?`,
|
||||||
id, decodeUID(uid), t.StateDeleted)
|
id, decodeUID(uid), t.StateDeleted)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -200,9 +201,11 @@ func (a *adapter) tenantUserGetAllTx(ctx context.Context, tx *sqlx.Tx, id t.Tena
|
|||||||
var q string
|
var q string
|
||||||
var params []any
|
var params []any
|
||||||
if a.tenantScopedDB {
|
if a.tenantScopedDB {
|
||||||
q, params, _ = sqlx.In("SELECT * FROM im_users WHERE id IN (?) AND state!=?", args, t.StateDeleted)
|
q, params, _ = sqlx.In(`SELECT id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||||
|
COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE id IN (?) AND state!=?`, args, t.StateDeleted)
|
||||||
} else {
|
} else {
|
||||||
q, params, _ = sqlx.In("SELECT * FROM im_users WHERE tenant_id=? AND id IN (?) AND state!=?", id, args, t.StateDeleted)
|
q, params, _ = sqlx.In(`SELECT tenant_id,id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||||
|
COALESCE(phone,'') AS phone,public,trusted FROM im_users WHERE tenant_id=? AND id IN (?) AND state!=?`, id, args, t.StateDeleted)
|
||||||
}
|
}
|
||||||
query := a.db.Rebind(q)
|
query := a.db.Rebind(q)
|
||||||
var rows *sqlx.Rows
|
var rows *sqlx.Rows
|
||||||
@@ -308,9 +311,6 @@ func (a *adapter) TenantUserDelete(id t.TenantID, uid t.Uid, hard bool) error {
|
|||||||
if _, err = tx.Exec("DELETE FROM im_credentials WHERE userid=?", decoded); err != nil {
|
if _, err = tx.Exec("DELETE FROM im_credentials WHERE userid=?", decoded); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err = tx.Exec("DELETE FROM im_usertags WHERE tenant_id=? AND userid=?", id, decoded); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err = tx.Exec("DELETE FROM im_users WHERE tenant_id=? AND id=?", id, decoded); err != nil {
|
if _, err = tx.Exec("DELETE FROM im_users WHERE tenant_id=? AND id=?", id, decoded); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,11 @@ func (a *adapter) TenantUserUpdate(id t.TenantID, uid t.Uid, update map[string]a
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
decoded := decodeUID(uid)
|
decoded := decodeUID(uid)
|
||||||
|
update = withoutUserTags(update)
|
||||||
cols, args := common.UpdateByMap(update)
|
cols, args := common.UpdateByMap(update)
|
||||||
|
if len(cols) == 0 {
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
args = append(args, id, decoded)
|
args = append(args, id, decoded)
|
||||||
res, err := tx.Exec("UPDATE im_users SET "+strings.Join(cols, ",")+" WHERE tenant_id=? AND id=?", args...)
|
res, err := tx.Exec("UPDATE im_users SET "+strings.Join(cols, ",")+" WHERE tenant_id=? AND id=?", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -404,14 +408,6 @@ func (a *adapter) TenantUserUpdate(id t.TenantID, uid t.Uid, update map[string]a
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tags := common.ExtractTags(update); tags != nil {
|
|
||||||
if _, err = tx.Exec("DELETE FROM im_usertags WHERE tenant_id=? AND userid=?", id, decoded); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = addTenantTags(tx, id, "im_usertags", "userid", decoded, tags, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,43 +422,7 @@ func (a *adapter) TenantUserUpdateTags(id t.TenantID, uid t.Uid, add, remove, re
|
|||||||
if a.tenantScopedDB {
|
if a.tenantScopedDB {
|
||||||
return a.UserUpdateTags(uid, add, remove, reset)
|
return a.UserUpdateTags(uid, add, remove, reset)
|
||||||
}
|
}
|
||||||
ctx, cancel := a.getContextForTx()
|
return nil, nil
|
||||||
if cancel != nil {
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
tx, err := a.db.BeginTxx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
decoded := decodeUID(uid)
|
|
||||||
if reset != nil {
|
|
||||||
if _, err = tx.Exec("DELETE FROM im_usertags WHERE tenant_id=? AND userid=?", id, decoded); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
add, remove = reset, nil
|
|
||||||
}
|
|
||||||
if err = addTenantTags(tx, id, "im_usertags", "userid", decoded, add, reset == nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(remove) > 0 {
|
|
||||||
q, params, _ := sqlx.In("DELETE FROM im_usertags WHERE tenant_id=? AND userid=? AND tag IN (?)", id, decoded, remove)
|
|
||||||
if _, err = tx.Exec(tx.Rebind(q), params...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var allTags []string
|
|
||||||
if err = tx.Select(&allTags, "SELECT tag FROM im_usertags WHERE tenant_id=? AND userid=?", id, decoded); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err = tx.Exec("UPDATE im_users SET tags=? WHERE tenant_id=? AND id=?", t.StringSlice(allTags), id, decoded); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return allTags, tx.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) TenantUserUnreadCount(id t.TenantID, uids ...t.Uid) (map[t.Uid]int, error) {
|
func (a *adapter) TenantUserUnreadCount(id t.TenantID, uids ...t.Uid) (map[t.Uid]int, error) {
|
||||||
@@ -1520,7 +1480,7 @@ func (a *adapter) TenantFind(id t.TenantID, caller, promoPrefix string, req [][]
|
|||||||
stateConstraint := ""
|
stateConstraint := ""
|
||||||
if activeOnly {
|
if activeOnly {
|
||||||
args = append(args, t.StateOK)
|
args = append(args, t.StateOK)
|
||||||
stateConstraint = "u.state=? AND "
|
stateConstraint = "t.state=? AND "
|
||||||
}
|
}
|
||||||
index := make(map[string]struct{})
|
index := make(map[string]struct{})
|
||||||
for _, tag := range allTags {
|
for _, tag := range allTags {
|
||||||
@@ -1531,27 +1491,7 @@ func (a *adapter) TenantFind(id t.TenantID, caller, promoPrefix string, req [][]
|
|||||||
if promoPrefix != "" {
|
if promoPrefix != "" {
|
||||||
matcher = "SUM(CASE WHEN LOCATE('" + promoPrefix + "', tg.tag)=1 THEN 20 ELSE 1 END)"
|
matcher = "SUM(CASE WHEN LOCATE('" + promoPrefix + "', tg.tag)=1 THEN 20 ELSE 1 END)"
|
||||||
}
|
}
|
||||||
query := "SELECT u.id,u.createdat,u.updatedat,0,u.access,0 AS subcnt,u.public,u.trusted,u.tags," + matcher + " AS matches " +
|
query := "SELECT t.name AS topic,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags," + matcher + " AS matches " +
|
||||||
"FROM im_users AS u JOIN im_usertags AS tg ON tg.tenant_id=u.tenant_id AND tg.userid=u.id " +
|
|
||||||
"WHERE u.tenant_id=? AND " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allTags)-1) + ") " +
|
|
||||||
"GROUP BY u.id,u.createdat,u.updatedat,u.access,u.public,u.trusted,u.tags "
|
|
||||||
if len(allReq) > 0 {
|
|
||||||
q, addArgs := common.DisjunctionSql(req, "tg.tag")
|
|
||||||
query += q
|
|
||||||
args = append(args, addArgs...)
|
|
||||||
}
|
|
||||||
query += "UNION ALL "
|
|
||||||
args = append(args, id)
|
|
||||||
if activeOnly {
|
|
||||||
args = append(args, t.StateOK)
|
|
||||||
stateConstraint = "t.state=? AND "
|
|
||||||
} else {
|
|
||||||
stateConstraint = ""
|
|
||||||
}
|
|
||||||
for _, tag := range allTags {
|
|
||||||
args = append(args, tag)
|
|
||||||
}
|
|
||||||
query += "SELECT t.name AS topic,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags," + matcher + " AS matches " +
|
|
||||||
"FROM im_topics AS t JOIN im_topictags AS tg ON t.tenant_id=tg.tenant_id AND t.name=tg.topic " +
|
"FROM im_topics AS t JOIN im_topictags AS tg ON t.tenant_id=tg.tenant_id AND t.name=tg.topic " +
|
||||||
"WHERE t.tenant_id=? AND " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allTags)-1) + ") " +
|
"WHERE t.tenant_id=? AND " + stateConstraint + "tg.tag IN (?" + strings.Repeat(",?", len(allTags)-1) + ") " +
|
||||||
"GROUP BY t.name,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags "
|
"GROUP BY t.name,t.createdat,t.updatedat,t.usebt,t.access,t.subcnt,t.public,t.trusted,t.tags "
|
||||||
@@ -1622,9 +1562,7 @@ func (a *adapter) TenantFindOne(id t.TenantID, tag string) (string, error) {
|
|||||||
}
|
}
|
||||||
rows, err := a.db.QueryxContext(ctx,
|
rows, err := a.db.QueryxContext(ctx,
|
||||||
"SELECT tp.name AS topic FROM im_topics AS tp LEFT JOIN im_topictags AS tt ON tp.tenant_id=tt.tenant_id AND tp.name=tt.topic "+
|
"SELECT tp.name AS topic FROM im_topics AS tp LEFT JOIN im_topictags AS tt ON tp.tenant_id=tt.tenant_id AND tp.name=tt.topic "+
|
||||||
"WHERE tp.tenant_id=? AND tt.tag=? UNION ALL "+
|
"WHERE tp.tenant_id=? AND tt.tag=? LIMIT 1", id, tag)
|
||||||
"SELECT u.id AS topic FROM im_users AS u LEFT JOIN im_usertags AS ut ON u.tenant_id=ut.tenant_id AND ut.userid=u.id "+
|
|
||||||
"WHERE u.tenant_id=? AND ut.tag=? LIMIT 1", id, tag, id, tag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -3816,7 +3754,11 @@ func (a *adapter) TenantUserProfileGet(id t.TenantID, uid t.Uid) (*t.UserProfile
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(user_id,public_account) VALUES(?,?)`, decodeUID(uid), uid.UserId()); err != nil {
|
profileID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account) VALUES(?,?,?)`, profileID, decodeUID(uid), uid.UserId()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return a.userProfileByUID(ctx, uid)
|
return a.userProfileByUID(ctx, uid)
|
||||||
@@ -3839,7 +3781,11 @@ func (a *adapter) TenantUserProfileUpdate(id t.TenantID, uid t.Uid, patch t.User
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(user_id,public_account) VALUES(?,?)`, decodeUID(uid), uid.UserId()); err != nil {
|
profileID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account) VALUES(?,?,?)`, profileID, decodeUID(uid), uid.UserId()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sets := []string{"version=version+1", "updated_at=CURRENT_TIMESTAMP(3)"}
|
sets := []string{"version=version+1", "updated_at=CURRENT_TIMESTAMP(3)"}
|
||||||
@@ -3873,6 +3819,10 @@ func (a *adapter) TenantUserProfileUpdate(id t.TenantID, uid t.Uid, patch t.User
|
|||||||
sets = append(sets, "region=?")
|
sets = append(sets, "region=?")
|
||||||
args = append(args, strings.TrimSpace(*patch.Region))
|
args = append(args, strings.TrimSpace(*patch.Region))
|
||||||
}
|
}
|
||||||
|
if patch.Email != nil {
|
||||||
|
sets = append(sets, "email=?")
|
||||||
|
args = append(args, strings.TrimSpace(*patch.Email))
|
||||||
|
}
|
||||||
args = append(args, decodeUID(uid), patch.Version)
|
args = append(args, decodeUID(uid), patch.Version)
|
||||||
res, err := a.db.ExecContext(ctx, `UPDATE im_user_profiles SET `+strings.Join(sets, ",")+` WHERE user_id=? AND version=?`, args...)
|
res, err := a.db.ExecContext(ctx, `UPDATE im_user_profiles SET `+strings.Join(sets, ",")+` WHERE user_id=? AND version=?`, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3902,7 +3852,11 @@ func (a *adapter) TenantUserPrivacyGet(id t.TenantID, uid t.Uid) (*t.UserPrivacy
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_privacy_settings(user_id,add_me_methods) VALUES(?,'{"account":true,"phone":true,"qr":true,"group":true}')`, decodeUID(uid)); err != nil {
|
privacyID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_privacy_settings(id,user_id,add_me_methods) VALUES(?,?,'{"account":true,"phone":true,"qr":true,"group":true}')`, privacyID, decodeUID(uid)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return a.userPrivacyByUID(ctx, uid)
|
return a.userPrivacyByUID(ctx, uid)
|
||||||
@@ -3925,7 +3879,11 @@ func (a *adapter) TenantUserPrivacyUpdate(id t.TenantID, uid t.Uid, patch t.User
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_privacy_settings(user_id,add_me_methods) VALUES(?,'{"account":true,"phone":true,"qr":true,"group":true}')`, decodeUID(uid)); err != nil {
|
privacyID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_privacy_settings(id,user_id,add_me_methods) VALUES(?,?,'{"account":true,"phone":true,"qr":true,"group":true}')`, privacyID, decodeUID(uid)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sets := []string{"version=version+1", "updated_at=CURRENT_TIMESTAMP(3)"}
|
sets := []string{"version=version+1", "updated_at=CURRENT_TIMESTAMP(3)"}
|
||||||
@@ -4048,12 +4006,16 @@ func (a *adapter) TenantDevicePushTokenUpsert(id t.TenantID, token *t.DevicePush
|
|||||||
token.Token = strings.TrimSpace(token.Token)
|
token.Token = strings.TrimSpace(token.Token)
|
||||||
token.AppVersion = strings.TrimSpace(token.AppVersion)
|
token.AppVersion = strings.TrimSpace(token.AppVersion)
|
||||||
token.Platform = strings.ToLower(strings.TrimSpace(token.Platform))
|
token.Platform = strings.ToLower(strings.TrimSpace(token.Platform))
|
||||||
_, err := a.db.ExecContext(ctx,
|
tokenID, err := nextSnowflakeID()
|
||||||
`INSERT INTO im_device_push_tokens(device_id,user_id,provider,token,app_version,platform,created_at,updated_at)
|
if err != nil {
|
||||||
VALUES(?,?,?,?,?,?,?,?)
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO im_device_push_tokens(id,device_id,user_id,provider,token,app_version,platform,created_at,updated_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE device_id=VALUES(device_id),user_id=VALUES(user_id),provider=VALUES(provider),token=VALUES(token),
|
ON DUPLICATE KEY UPDATE device_id=VALUES(device_id),user_id=VALUES(user_id),provider=VALUES(provider),token=VALUES(token),
|
||||||
app_version=VALUES(app_version),platform=VALUES(platform),updated_at=VALUES(updated_at)`,
|
app_version=VALUES(app_version),platform=VALUES(platform),updated_at=VALUES(updated_at)`,
|
||||||
token.DeviceID, decodeUID(token.UserID), token.Provider, token.Token, token.AppVersion, token.Platform, now, now)
|
tokenID, token.DeviceID, decodeUID(token.UserID), token.Provider, token.Token, token.AppVersion, token.Platform, now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -4515,12 +4477,14 @@ func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfi
|
|||||||
Signature string
|
Signature string
|
||||||
Gender string
|
Gender string
|
||||||
Region string
|
Region string
|
||||||
|
Email string
|
||||||
PublicAccount string `db:"public_account"`
|
PublicAccount string `db:"public_account"`
|
||||||
Version int
|
Version int
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}{}
|
}{}
|
||||||
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT * FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil {
|
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,avatar_file_id,nickname,birth_date,signature,gender,region,email,
|
||||||
|
public_account,version,created_at,updated_at FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
profile := &t.UserProfile{
|
profile := &t.UserProfile{
|
||||||
@@ -4530,6 +4494,7 @@ func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfi
|
|||||||
Signature: row.Signature,
|
Signature: row.Signature,
|
||||||
Gender: row.Gender,
|
Gender: row.Gender,
|
||||||
Region: row.Region,
|
Region: row.Region,
|
||||||
|
Email: row.Email,
|
||||||
PublicAccount: row.PublicAccount,
|
PublicAccount: row.PublicAccount,
|
||||||
Version: row.Version,
|
Version: row.Version,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: row.CreatedAt,
|
||||||
@@ -4552,7 +4517,8 @@ func (a *adapter) userPrivacyByUID(ctx context.Context, uid t.Uid) (*t.UserPriva
|
|||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}{}
|
}{}
|
||||||
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT * FROM im_user_privacy_settings WHERE user_id=?`, decodeUID(uid)); err != nil {
|
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,add_me_methods,profile_visibility,last_seen_visibility,
|
||||||
|
read_receipts,version,created_at,updated_at FROM im_user_privacy_settings WHERE user_id=?`, decodeUID(uid)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
settings := &t.UserPrivacySettings{
|
settings := &t.UserPrivacySettings{
|
||||||
@@ -5615,11 +5581,15 @@ func (a *adapter) TenantUserSecurityGetAuthVersion(id t.TenantID, uid t.Uid) (in
|
|||||||
}
|
}
|
||||||
decoded := decodeUID(uid)
|
decoded := decodeUID(uid)
|
||||||
now := t.TimeNow()
|
now := t.TimeNow()
|
||||||
_, err := a.db.ExecContext(ctx,
|
securityID, err := nextSnowflakeID()
|
||||||
`INSERT INTO im_user_security(user_id,auth_version,created_at,updated_at)
|
if err != nil {
|
||||||
VALUES(?,?,?,?)
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx,
|
||||||
|
`INSERT INTO im_user_security(id,user_id,auth_version,created_at,updated_at)
|
||||||
|
VALUES(?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE auth_version=auth_version`,
|
ON DUPLICATE KEY UPDATE auth_version=auth_version`,
|
||||||
decoded, 1, now, now)
|
securityID, decoded, 1, now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -5666,14 +5636,18 @@ func (a *adapter) TenantUserSecurityBumpAuthVersion(id t.TenantID, uid t.Uid, up
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
decoded := decodeUID(uid)
|
decoded := decodeUID(uid)
|
||||||
|
securityID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if _, err = tx.ExecContext(ctx,
|
if _, err = tx.ExecContext(ctx,
|
||||||
`INSERT INTO im_user_security(user_id,auth_version,password_updated_at,created_at,updated_at)
|
`INSERT INTO im_user_security(id,user_id,auth_version,password_updated_at,created_at,updated_at)
|
||||||
VALUES(?,?,?,?,?)
|
VALUES(?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
auth_version=auth_version+1,
|
auth_version=auth_version+1,
|
||||||
password_updated_at=VALUES(password_updated_at),
|
password_updated_at=VALUES(password_updated_at),
|
||||||
updated_at=VALUES(updated_at)`,
|
updated_at=VALUES(updated_at)`,
|
||||||
decoded, 2, updatedAt, updatedAt, updatedAt); err != nil {
|
securityID, decoded, 2, updatedAt, updatedAt, updatedAt); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
var version int
|
var version int
|
||||||
@@ -5911,12 +5885,16 @@ func (a *adapter) TenantIndependentPasswordUpsert(id t.TenantID, pass *t.Indepen
|
|||||||
now := t.TimeNow()
|
now := t.TimeNow()
|
||||||
pass.CreatedAt = now
|
pass.CreatedAt = now
|
||||||
pass.UpdatedAt = now
|
pass.UpdatedAt = now
|
||||||
_, err := a.db.ExecContext(ctx,
|
settingsID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx,
|
||||||
`INSERT INTO im_youth_mode_settings
|
`INSERT INTO im_youth_mode_settings
|
||||||
(user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at)
|
(id,user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at)
|
||||||
VALUES(?,?,?,0,NULL,?,?)
|
VALUES(?,?,?,?,0,NULL,?,?)
|
||||||
ON DUPLICATE KEY UPDATE password_hash=VALUES(password_hash),failed_attempts=0,locked_until=NULL,updated_at=VALUES(updated_at)`,
|
ON DUPLICATE KEY UPDATE password_hash=VALUES(password_hash),failed_attempts=0,locked_until=NULL,updated_at=VALUES(updated_at)`,
|
||||||
decodeUID(pass.UserID), pass.Purpose, pass.PasswordHash, pass.CreatedAt, pass.UpdatedAt)
|
settingsID, decodeUID(pass.UserID), pass.Purpose, pass.PasswordHash, pass.CreatedAt, pass.UpdatedAt)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6478,10 +6456,14 @@ func (a *adapter) TenantFriendRequestAccept(id t.TenantID, requestID string, act
|
|||||||
row.UpdatedAt = handledAt
|
row.UpdatedAt = handledAt
|
||||||
}
|
}
|
||||||
for _, pair := range [][2]t.Uid{{applicant, target}, {target, applicant}} {
|
for _, pair := range [][2]t.Uid{{applicant, target}, {target, applicant}} {
|
||||||
|
friendshipID, idErr := nextSnowflakeID()
|
||||||
|
if idErr != nil {
|
||||||
|
return nil, idErr
|
||||||
|
}
|
||||||
if _, err = tx.ExecContext(ctx,
|
if _, err = tx.ExecContext(ctx,
|
||||||
"INSERT INTO im_friendships(user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?) "+
|
"INSERT INTO im_friendships(id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+
|
||||||
"ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)",
|
"ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)",
|
||||||
decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, handledAt, handledAt); err != nil {
|
friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, handledAt, handledAt); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6642,10 +6624,14 @@ func (a *adapter) TenantFriendshipUpsertPair(id t.TenantID, uid1, uid2 t.Uid, to
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
for _, pair := range [][2]t.Uid{{uid1, uid2}, {uid2, uid1}} {
|
for _, pair := range [][2]t.Uid{{uid1, uid2}, {uid2, uid1}} {
|
||||||
|
friendshipID, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := a.db.ExecContext(ctx,
|
if _, err := a.db.ExecContext(ctx,
|
||||||
"INSERT INTO im_friendships(user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?) "+
|
"INSERT INTO im_friendships(id,user_id,peer_id,status,topic,created_at,updated_at) VALUES(?,?,?,?,?,?,?) "+
|
||||||
"ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)",
|
"ON DUPLICATE KEY UPDATE status=VALUES(status),topic=VALUES(topic),updated_at=VALUES(updated_at)",
|
||||||
decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil {
|
friendshipID, decodeUID(pair[0]), decodeUID(pair[1]), t.RelationshipFriend, topic, now, now); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6680,13 +6666,13 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init
|
|||||||
}
|
}
|
||||||
if q := strings.TrimSpace(queryText); q != "" {
|
if q := strings.TrimSpace(queryText); q != "" {
|
||||||
like := "%" + q + "%"
|
like := "%" + q + "%"
|
||||||
where = append(where, "(fs.remark LIKE ? OR CAST(u.public AS CHAR) LIKE ? OR CAST(u.tags AS CHAR) LIKE ?)")
|
where = append(where, "(fs.remark LIKE ? OR p.nickname LIKE ? OR p.public_account LIKE ? OR CAST(u.public AS CHAR) LIKE ?)")
|
||||||
args = append(args, like, like, like)
|
args = append(args, like, like, like, like)
|
||||||
}
|
}
|
||||||
if initial = strings.TrimSpace(initial); initial != "" {
|
if initial = strings.TrimSpace(initial); initial != "" {
|
||||||
like := initial + "%"
|
like := initial + "%"
|
||||||
where = append(where, "(fs.remark LIKE ? OR CAST(u.public AS CHAR) LIKE ?)")
|
where = append(where, "(fs.remark LIKE ? OR p.nickname LIKE ? OR CAST(u.public AS CHAR) LIKE ?)")
|
||||||
args = append(args, like, like)
|
args = append(args, like, like, like)
|
||||||
}
|
}
|
||||||
args = append(args, limit+1)
|
args = append(args, limit+1)
|
||||||
ctx, cancel := a.getContext()
|
ctx, cancel := a.getContext()
|
||||||
@@ -6696,6 +6682,7 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init
|
|||||||
rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted
|
rows, err := a.db.QueryxContext(ctx, `SELECT fs.*,u.public,u.trusted
|
||||||
FROM im_friendships AS fs
|
FROM im_friendships AS fs
|
||||||
JOIN im_users AS u ON u.id=fs.peer_id
|
JOIN im_users AS u ON u.id=fs.peer_id
|
||||||
|
LEFT JOIN im_user_profiles AS p ON p.user_id=u.id
|
||||||
WHERE `+strings.Join(where, " AND ")+`
|
WHERE `+strings.Join(where, " AND ")+`
|
||||||
ORDER BY fs.peer_id ASC LIMIT ?`, args...)
|
ORDER BY fs.peer_id ASC LIMIT ?`, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6776,7 +6763,8 @@ func (a *adapter) TenantFriendshipGet(id t.TenantID, userID, peerID t.Uid) (*t.F
|
|||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}{}
|
}{}
|
||||||
err := a.db.GetContext(ctx, &row, "SELECT * FROM im_friendships WHERE user_id=? AND peer_id=?",
|
err := a.db.GetContext(ctx, &row, `SELECT user_id,peer_id,status,remark,topic,created_at,updated_at
|
||||||
|
FROM im_friendships WHERE user_id=? AND peer_id=?`,
|
||||||
decodeUID(userID), decodeUID(peerID))
|
decodeUID(userID), decodeUID(peerID))
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -6871,7 +6859,8 @@ func (a *adapter) TenantUserBlockGet(id t.TenantID, blockerID, blockedID t.Uid)
|
|||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}{}
|
}{}
|
||||||
err := a.db.GetContext(ctx, &row, "SELECT * FROM im_user_blocks WHERE blocker_id=? AND blocked_id=?",
|
err := a.db.GetContext(ctx, &row, `SELECT blocker_id,blocked_id,created_at,updated_at
|
||||||
|
FROM im_user_blocks WHERE blocker_id=? AND blocked_id=?`,
|
||||||
decodeUID(blockerID), decodeUID(blockedID))
|
decodeUID(blockerID), decodeUID(blockedID))
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -6919,7 +6908,7 @@ func (a *adapter) TenantUserBlockList(id t.TenantID, blockerID t.Uid, cursor str
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
rows, err := a.db.QueryxContext(ctx, `SELECT b.*,u.public,u.trusted
|
rows, err := a.db.QueryxContext(ctx, `SELECT b.blocker_id,b.blocked_id,b.created_at,b.updated_at,u.public,u.trusted
|
||||||
FROM im_user_blocks AS b
|
FROM im_user_blocks AS b
|
||||||
LEFT JOIN im_users AS u ON u.id=b.blocked_id
|
LEFT JOIN im_users AS u ON u.id=b.blocked_id
|
||||||
WHERE `+strings.Join(where, " AND ")+`
|
WHERE `+strings.Join(where, " AND ")+`
|
||||||
@@ -6986,10 +6975,14 @@ func (a *adapter) TenantUserBlockUpsert(id t.TenantID, blockerID, blockedID t.Ui
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx,
|
blockID, err := nextSnowflakeID()
|
||||||
"INSERT INTO im_user_blocks(blocker_id,blocked_id,created_at,updated_at) VALUES(?,?,?,?) "+
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx,
|
||||||
|
"INSERT INTO im_user_blocks(id,blocker_id,blocked_id,created_at,updated_at) VALUES(?,?,?,?,?) "+
|
||||||
"ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)",
|
"ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)",
|
||||||
decodeUID(blockerID), decodeUID(blockedID), now, now)
|
blockID, decodeUID(blockerID), decodeUID(blockedID), now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -7048,24 +7041,23 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc
|
|||||||
where = append(where, "u.id>?")
|
where = append(where, "u.id>?")
|
||||||
args = append(args, parsed)
|
args = append(args, parsed)
|
||||||
}
|
}
|
||||||
joins := []string{"LEFT JOIN im_user_privacy_settings AS privacy ON privacy.user_id=u.id"}
|
joins := []string{
|
||||||
|
"LEFT JOIN im_user_privacy_settings AS privacy ON privacy.user_id=u.id",
|
||||||
|
"LEFT JOIN im_user_profiles AS profile ON profile.user_id=u.id",
|
||||||
|
}
|
||||||
addMethod := "account"
|
addMethod := "account"
|
||||||
switch searchType {
|
switch searchType {
|
||||||
case "", "account":
|
case "", "account":
|
||||||
like := "%" + queryText + "%"
|
like := "%" + queryText + "%"
|
||||||
joins = append(joins, "LEFT JOIN im_usertags AS ut ON ut.userid=u.id")
|
where = append(where, "(profile.nickname LIKE ? OR profile.public_account LIKE ? OR CAST(u.public AS CHAR) LIKE ? OR u.id=?)")
|
||||||
where = append(where, "(ut.tag LIKE ? OR CAST(u.public AS CHAR) LIKE ? OR u.id=?)")
|
args = append(args, like, like, like, userIDNumericArg(queryText))
|
||||||
args = append(args, like, like, userIDNumericArg(queryText))
|
|
||||||
case "exact_account":
|
case "exact_account":
|
||||||
joins = append(joins, "LEFT JOIN im_usertags AS ut ON ut.userid=u.id")
|
joins = append(joins, "LEFT JOIN im_auth AS account_auth ON account_auth.userid=u.id AND account_auth.scheme='basic'")
|
||||||
where = append(where, "(ut.tag=? OR u.id=?)")
|
where = append(where, "(profile.public_account=? OR account_auth.uname=? OR u.id=?)")
|
||||||
args = append(args, queryText, userIDNumericArg(queryText))
|
args = append(args, queryText, strings.TrimPrefix(queryText, "basic:"), userIDNumericArg(queryText))
|
||||||
case "phone":
|
case "phone":
|
||||||
addMethod = "phone"
|
addMethod = "phone"
|
||||||
where = append(where, `EXISTS (
|
where = append(where, "u.phone=?")
|
||||||
SELECT 1 FROM im_credentials AS c
|
|
||||||
WHERE c.userid=u.id AND c.method='tel' AND c.value=? AND c.done=true
|
|
||||||
)`)
|
|
||||||
args = append(args, queryText)
|
args = append(args, queryText)
|
||||||
default:
|
default:
|
||||||
return nil, "", t.ErrMalformed
|
return nil, "", t.ErrMalformed
|
||||||
@@ -7080,7 +7072,8 @@ func (a *adapter) TenantUserSearch(id t.TenantID, viewer t.Uid, queryText, searc
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
rows, err := a.db.QueryxContext(ctx, `SELECT DISTINCT u.* FROM im_users AS u `+strings.Join(joins, " ")+`
|
rows, err := a.db.QueryxContext(ctx, `SELECT DISTINCT u.id,u.createdat,u.updatedat,u.state,u.stateat,u.access,u.lastseen,u.useragent,
|
||||||
|
COALESCE(u.phone,'') AS phone,u.public,u.trusted FROM im_users AS u `+strings.Join(joins, " ")+`
|
||||||
WHERE `+strings.Join(where, " AND ")+` ORDER BY u.id ASC LIMIT ?`, args...)
|
WHERE `+strings.Join(where, " AND ")+` ORDER BY u.id ASC LIMIT ?`, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ import (
|
|||||||
t "github.com/tinode/chat/server/store/types"
|
t "github.com/tinode/chat/server/store/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
groupSettingsSelectColumns = `topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,
|
||||||
|
member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,
|
||||||
|
created_by,dissolved_by,dissolved_at,created_at,updated_at`
|
||||||
|
groupMuteSelectColumns = `topic,user_id,muted_by,muted_at,expires_at,reason,status,revoked_by,revoked_at,version,created_at,updated_at`
|
||||||
|
)
|
||||||
|
|
||||||
func (a *adapter) TenantGroupSettingsCreate(id t.TenantID, settings *t.GroupSettings) (*t.GroupSettings, error) {
|
func (a *adapter) TenantGroupSettingsCreate(id t.TenantID, settings *t.GroupSettings) (*t.GroupSettings, error) {
|
||||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -45,11 +52,15 @@ func (a *adapter) TenantGroupSettingsCreate(id t.TenantID, settings *t.GroupSett
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_settings
|
settingsID, err := nextSnowflakeID()
|
||||||
(topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,created_by,created_at,updated_at)
|
if err != nil {
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx, `INSERT INTO im_group_settings
|
||||||
|
(id,topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,created_by,created_at,updated_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
ON DUPLICATE KEY UPDATE name=VALUES(name),avatar_file_id=VALUES(avatar_file_id),description=VALUES(description),updated_at=VALUES(updated_at)`,
|
ON DUPLICATE KEY UPDATE name=VALUES(name),avatar_file_id=VALUES(avatar_file_id),description=VALUES(description),updated_at=VALUES(updated_at)`,
|
||||||
settings.Topic, decodeUID(settings.OwnerUserID), settings.Name, settings.AvatarFileID, settings.Description, settings.GroupStatus,
|
settingsID, settings.Topic, decodeUID(settings.OwnerUserID), settings.Name, settings.AvatarFileID, settings.Description, settings.GroupStatus,
|
||||||
settings.JoinApproval, settings.MuteAll, settings.MemberUploadFiles, settings.MemberPrivateChat, settings.MemberInvite,
|
settings.JoinApproval, settings.MuteAll, settings.MemberUploadFiles, settings.MemberPrivateChat, settings.MemberInvite,
|
||||||
settings.MaxMemberCount, settings.MaxAdminCount, settings.MemberCount, settings.Version, decodeUID(settings.CreatedBy), settings.CreatedAt, settings.UpdatedAt)
|
settings.MaxMemberCount, settings.MaxAdminCount, settings.MemberCount, settings.Version, decodeUID(settings.CreatedBy), settings.CreatedAt, settings.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -626,8 +637,12 @@ func (a *adapter) TenantGroupAnnouncementRead(id t.TenantID, topic, announcement
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
_, err := a.db.ExecContext(ctx, `INSERT INTO im_group_announcement_reads(announcement_id,topic,user_id,read_at)
|
readID, err := nextSnowflakeID()
|
||||||
VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE read_at=VALUES(read_at),updated_at=CURRENT_TIMESTAMP(3)`, announcementID, topic, decodeUID(uid), readAt)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.db.ExecContext(ctx, `INSERT INTO im_group_announcement_reads(id,announcement_id,topic,user_id,read_at)
|
||||||
|
VALUES(?,?,?,?,?) ON DUPLICATE KEY UPDATE read_at=VALUES(read_at),updated_at=CURRENT_TIMESTAMP(3)`, readID, announcementID, topic, decodeUID(uid), readAt)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +667,7 @@ func (a *adapter) TenantGroupMuteList(id t.TenantID, topic, cursor string, limit
|
|||||||
if cancel != nil {
|
if cancel != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
rows, err := a.db.QueryxContext(ctx, "SELECT * FROM im_group_mutes WHERE "+strings.Join(where, " AND ")+" ORDER BY user_id LIMIT ?", args...)
|
rows, err := a.db.QueryxContext(ctx, "SELECT "+groupMuteSelectColumns+" FROM im_group_mutes WHERE "+strings.Join(where, " AND ")+" ORDER BY user_id LIMIT ?", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
@@ -696,10 +711,14 @@ func (a *adapter) TenantGroupMuteUpsert(id t.TenantID, mute *t.GroupMute) (*t.Gr
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
_, err = tx.ExecContext(ctx, `INSERT INTO im_group_mutes(topic,user_id,muted_by,muted_at,expires_at,reason,status,version)
|
muteID, err := nextSnowflakeID()
|
||||||
VALUES(?,?,?,?,?,?,?,1)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = tx.ExecContext(ctx, `INSERT INTO im_group_mutes(id,topic,user_id,muted_by,muted_at,expires_at,reason,status,version)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,1)
|
||||||
ON DUPLICATE KEY UPDATE muted_by=VALUES(muted_by),muted_at=VALUES(muted_at),expires_at=VALUES(expires_at),reason=VALUES(reason),status=VALUES(status),revoked_by=NULL,revoked_at=NULL,version=version+1,updated_at=CURRENT_TIMESTAMP(3)`,
|
ON DUPLICATE KEY UPDATE muted_by=VALUES(muted_by),muted_at=VALUES(muted_at),expires_at=VALUES(expires_at),reason=VALUES(reason),status=VALUES(status),revoked_by=NULL,revoked_at=NULL,version=version+1,updated_at=CURRENT_TIMESTAMP(3)`,
|
||||||
mute.Topic, decodeUID(mute.UserID), decodeUID(mute.MutedBy), now, mute.ExpiresAt, mute.Reason, mute.Status)
|
muteID, mute.Topic, decodeUID(mute.UserID), decodeUID(mute.MutedBy), now, mute.ExpiresAt, mute.Reason, mute.Status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -711,7 +730,7 @@ func (a *adapter) TenantGroupMuteUpsert(id t.TenantID, mute *t.GroupMute) (*t.Gr
|
|||||||
if err = tx.Commit(); err != nil {
|
if err = tx.Commit(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return a.groupMuteByQuery(ctx, "SELECT * FROM im_group_mutes WHERE topic=? AND user_id=?", mute.Topic, decodeUID(mute.UserID))
|
return a.groupMuteByQuery(ctx, "SELECT "+groupMuteSelectColumns+" FROM im_group_mutes WHERE topic=? AND user_id=?", mute.Topic, decodeUID(mute.UserID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *adapter) TenantGroupMuteDelete(id t.TenantID, topic string, uid, actor t.Uid, now time.Time) error {
|
func (a *adapter) TenantGroupMuteDelete(id t.TenantID, topic string, uid, actor t.Uid, now time.Time) error {
|
||||||
@@ -1090,9 +1109,13 @@ func (a *adapter) TenantGroupEventMarkDelivered(id t.TenantID, eventID string, n
|
|||||||
}
|
}
|
||||||
|
|
||||||
func insertGroupSettingsTx(tx *sqlx.Tx, settings *t.GroupSettings) error {
|
func insertGroupSettingsTx(tx *sqlx.Tx, settings *t.GroupSettings) error {
|
||||||
_, err := tx.Exec(`INSERT INTO im_group_settings
|
settingsID, err := nextSnowflakeID()
|
||||||
(topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,created_by,created_at,updated_at)
|
if err != nil {
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, settings.Topic, decodeUID(settings.OwnerUserID), settings.Name,
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`INSERT INTO im_group_settings
|
||||||
|
(id,topic,owner_user_id,name,avatar_file_id,description,group_status,join_approval,mute_all,member_upload_files,member_private_chat,member_invite,max_member_count,max_admin_count,member_count,version,created_by,created_at,updated_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, settingsID, settings.Topic, decodeUID(settings.OwnerUserID), settings.Name,
|
||||||
settings.AvatarFileID, settings.Description, settings.GroupStatus, settings.JoinApproval, settings.MuteAll,
|
settings.AvatarFileID, settings.Description, settings.GroupStatus, settings.JoinApproval, settings.MuteAll,
|
||||||
settings.MemberUploadFiles, settings.MemberPrivateChat, settings.MemberInvite, settings.MaxMemberCount,
|
settings.MemberUploadFiles, settings.MemberPrivateChat, settings.MemberInvite, settings.MaxMemberCount,
|
||||||
settings.MaxAdminCount, settings.MemberCount, settings.Version, decodeUID(settings.CreatedBy), settings.CreatedAt, settings.UpdatedAt)
|
settings.MaxAdminCount, settings.MemberCount, settings.Version, decodeUID(settings.CreatedBy), settings.CreatedAt, settings.UpdatedAt)
|
||||||
@@ -1143,7 +1166,7 @@ func (a *adapter) routeGroupTenant(id t.TenantID) (*adapter, error) {
|
|||||||
|
|
||||||
func (a *adapter) groupSettingsByTopic(ctx context.Context, topic string) (*t.GroupSettings, error) {
|
func (a *adapter) groupSettingsByTopic(ctx context.Context, topic string) (*t.GroupSettings, error) {
|
||||||
var item t.GroupSettings
|
var item t.GroupSettings
|
||||||
err := a.db.QueryRowxContext(ctx, "SELECT * FROM im_group_settings WHERE topic=?", topic).StructScan(&item)
|
err := a.db.QueryRowxContext(ctx, "SELECT "+groupSettingsSelectColumns+" FROM im_group_settings WHERE topic=?", topic).StructScan(&item)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, t.ErrNotFound
|
return nil, t.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,89 @@ func TestHTTPInfrastructureTablesCreated(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserIdentitySchema(t *testing.T) {
|
||||||
|
var count int
|
||||||
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='im_users' AND column_name='phone'`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatal("im_users.phone is missing")
|
||||||
|
}
|
||||||
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='im_user_profiles' AND column_name='email'`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatal("im_user_profiles.email is missing")
|
||||||
|
}
|
||||||
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='im_users' AND column_name='tags'`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Fatal("legacy im_users.tags still exists")
|
||||||
|
}
|
||||||
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='im_usertags'`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Fatal("legacy im_usertags still exists")
|
||||||
|
}
|
||||||
|
if err := db.Get(&count, `SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_schema=DATABASE() AND table_name='im_topictags'`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 1 {
|
||||||
|
t.Fatal("topic tag table must be retained")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllIMTablesUseIDPrimaryKey(t *testing.T) {
|
||||||
|
var tables []string
|
||||||
|
if err := db.Select(&tables, `SELECT t.table_name
|
||||||
|
FROM information_schema.tables AS t
|
||||||
|
WHERE t.table_schema=DATABASE() AND t.table_type='BASE TABLE' AND t.table_name LIKE 'im\_%'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns AS c
|
||||||
|
WHERE c.table_schema=t.table_schema AND c.table_name=t.table_name AND c.column_name='id'
|
||||||
|
)
|
||||||
|
ORDER BY t.table_name`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tables) != 0 {
|
||||||
|
t.Fatalf("IM tables without id column: %v", tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
tables = nil
|
||||||
|
if err := db.Select(&tables, `SELECT s.table_name
|
||||||
|
FROM information_schema.statistics AS s
|
||||||
|
WHERE s.table_schema=DATABASE() AND s.index_name='PRIMARY'
|
||||||
|
GROUP BY s.table_name
|
||||||
|
HAVING GROUP_CONCAT(s.column_name ORDER BY s.seq_in_index) <> 'id'
|
||||||
|
ORDER BY s.table_name`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(tables) != 0 {
|
||||||
|
t.Fatalf("IM tables whose primary key is not id: %v", tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := make([]string, 0, 11)
|
||||||
|
if err := db.Select(&targets, `SELECT table_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND column_name='id' AND extra LIKE '%auto_increment%'
|
||||||
|
AND table_name IN ('im_user_security','im_kvmeta','im_user_profiles','im_user_privacy_settings',
|
||||||
|
'im_device_push_tokens','im_friendships','im_user_blocks','im_youth_mode_settings',
|
||||||
|
'im_group_settings','im_group_announcement_reads','im_group_mutes')
|
||||||
|
ORDER BY table_name`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(targets) != 0 {
|
||||||
|
t.Fatalf("snowflake primary keys unexpectedly use AUTO_INCREMENT: %v", targets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTenantAuthIsolation(t *testing.T) {
|
func TestTenantAuthIsolation(t *testing.T) {
|
||||||
tenantAdp, ok := adp.(adapter.TenantAuthAdapter)
|
tenantAdp, ok := adp.(adapter.TenantAuthAdapter)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -192,8 +275,8 @@ func TestTenantAuthIsolation(t *testing.T) {
|
|||||||
userBID := userAID + 1
|
userBID := userAID + 1
|
||||||
for index, userID := range []int64{userAID, userBID} {
|
for index, userID := range []int64{userAID, userBID} {
|
||||||
tenantDB := quoteIdent(dbNames[index])
|
tenantDB := quoteIdent(dbNames[index])
|
||||||
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,createdat,updatedat,state,access,public,trusted,tags) "+
|
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_users(id,createdat,updatedat,state,access,phone,public,trusted) "+
|
||||||
"VALUES(?,?,?,?,?,?,?,?)", userID, now, now, types.StateOK, `{}`, nil, nil, nil); err != nil {
|
"VALUES(?,?,?,?,?,?,?,?)", userID, now, now, types.StateOK, `{}`, "13800000000", nil, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(uname,userid,scheme,authlvl,secret) VALUES(?,?,?,?,?)",
|
if _, err := db.Exec("INSERT INTO "+tenantDB+".im_auth(uname,userid,scheme,authlvl,secret) VALUES(?,?,?,?,?)",
|
||||||
@@ -740,8 +823,8 @@ func TestFind(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if len(got) != 3 {
|
if len(got) != 1 {
|
||||||
t.Error(mismatchErrorString("result length", len(got), 3))
|
t.Error(mismatchErrorString("topic-only result length", len(got), 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,54 +894,13 @@ func TestUserUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUserUpdateTags(t *testing.T) {
|
func TestUserUpdateTags(t *testing.T) {
|
||||||
addTags := testData.Tags[0]
|
|
||||||
removeTags := testData.Tags[1]
|
|
||||||
resetTags := testData.Tags[2]
|
|
||||||
uid := types.ParseUserId("usr" + testData.Users[0].Id)
|
uid := types.ParseUserId("usr" + testData.Users[0].Id)
|
||||||
|
got, err := adp.UserUpdateTags(uid, []string{"legacy"}, nil, nil)
|
||||||
got, err := adp.UserUpdateTags(uid, addTags, nil, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
want := []string{"alice", "tag1"}
|
if got != nil {
|
||||||
if !reflect.DeepEqual(got, want) {
|
t.Fatalf("user tags must stay disabled, got %v", got)
|
||||||
t.Error(mismatchErrorString("Tags", got, want))
|
|
||||||
|
|
||||||
}
|
|
||||||
got, err = adp.UserUpdateTags(uid, nil, removeTags, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want = nil
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Error(mismatchErrorString("Tags", got, want))
|
|
||||||
|
|
||||||
}
|
|
||||||
got, err = adp.UserUpdateTags(uid, nil, nil, resetTags)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want = []string{"alice", "tag111", "tag333"}
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Error(mismatchErrorString("Tags", got, want))
|
|
||||||
|
|
||||||
}
|
|
||||||
got, err = adp.UserUpdateTags(uid, addTags, removeTags, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want = []string{"tag111", "tag333"}
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Error(mismatchErrorString("Tags", got, want))
|
|
||||||
|
|
||||||
}
|
|
||||||
got, err = adp.UserUpdateTags(uid, addTags, removeTags, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want = []string{"tag111", "tag333"}
|
|
||||||
if !reflect.DeepEqual(got, want) {
|
|
||||||
t.Error(mismatchErrorString("Tags", got, want))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1505,6 +1547,11 @@ func init() {
|
|||||||
if adp.IsOpen() {
|
if adp.IsOpen() {
|
||||||
log.Print("Connection is already opened")
|
log.Print("Connection is already opened")
|
||||||
}
|
}
|
||||||
|
testData = test_data.InitTestData()
|
||||||
|
if testData == nil {
|
||||||
|
log.Fatal("Failed to initialize test data")
|
||||||
|
}
|
||||||
|
store.SetTestUidGenerator(*testData.UGen)
|
||||||
|
|
||||||
err := adp.Open(config.Adapters[adp.GetName()])
|
err := adp.Open(config.Adapters[adp.GetName()])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1512,9 +1559,4 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db = adp.GetTestDB().(*sqlx.DB)
|
db = adp.GetTestDB().(*sqlx.DB)
|
||||||
testData = test_data.InitTestData()
|
|
||||||
if testData == nil {
|
|
||||||
log.Fatal("Failed to initialize test data")
|
|
||||||
}
|
|
||||||
store.SetTestUidGenerator(*testData.UGen)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build mysql
|
||||||
|
// +build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
func withoutUserTags(update map[string]any) map[string]any {
|
||||||
|
if update == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
clean := make(map[string]any, len(update))
|
||||||
|
for key, value := range update {
|
||||||
|
if key != "Tags" && key != "tags" {
|
||||||
|
clean[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clean
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
//go:build mysql
|
||||||
|
// +build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/tinode/chat/server/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const userIdentityMigrationLock = "CONCAT('im_user_identity:', LEFT(SHA2(DATABASE(), 256), 40))"
|
||||||
|
|
||||||
|
type identityValue struct {
|
||||||
|
TenantID int64 `db:"tenant_id"`
|
||||||
|
UserID int64 `db:"user_id"`
|
||||||
|
Value string `db:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureUserIdentitySchema moves user phone/email out of the legacy tag store.
|
||||||
|
// Topic tags are intentionally unaffected.
|
||||||
|
func ensureUserIdentitySchema(db *sqlx.DB) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
conn, err := db.Connx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
var locked sql.NullInt64
|
||||||
|
if err = conn.QueryRowContext(ctx, "SELECT GET_LOCK("+userIdentityMigrationLock+", 60)").Scan(&locked); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !locked.Valid || locked.Int64 != 1 {
|
||||||
|
return errors.New("mysql: timed out waiting for user identity migration lock")
|
||||||
|
}
|
||||||
|
defer conn.ExecContext(ctx, "SELECT RELEASE_LOCK("+userIdentityMigrationLock+")")
|
||||||
|
|
||||||
|
usersExist, err := identityTableExists(ctx, conn, "im_users")
|
||||||
|
if err != nil || !usersExist {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
usersScoped, err := identityColumnExists(ctx, conn, "im_users", "tenant_id")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profilesExist, err := identityTableExists(ctx, conn, "im_user_profiles")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !profilesExist {
|
||||||
|
profileScope := ""
|
||||||
|
profileUniqueKey := "user_id"
|
||||||
|
if usersScoped {
|
||||||
|
profileScope = "tenant_id BIGINT NOT NULL,"
|
||||||
|
profileUniqueKey = "tenant_id,user_id"
|
||||||
|
}
|
||||||
|
if _, err = conn.ExecContext(ctx, `CREATE TABLE im_user_profiles(
|
||||||
|
id BIGINT NOT NULL,`+profileScope+`user_id BIGINT NOT NULL,
|
||||||
|
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
birth_date DATE,signature VARCHAR(256) NOT NULL DEFAULT '',gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||||
|
region VARCHAR(128) NOT NULL DEFAULT '',email VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
public_account VARCHAR(64) NOT NULL DEFAULT '',version INT NOT NULL DEFAULT 1,
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY(id),UNIQUE INDEX uk_im_user_profiles_user(`+profileUniqueKey+`))`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profilesExist = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists, err := identityColumnExists(ctx, conn, "im_users", "phone"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_users ADD COLUMN phone VARCHAR(32) NULL AFTER useragent"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if profilesExist {
|
||||||
|
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "email"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN email VARCHAR(128) NOT NULL DEFAULT '' AFTER region"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = backfillUserPhones(ctx, conn, usersScoped); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if profilesExist {
|
||||||
|
if err = backfillProfileEmails(ctx, conn, usersScoped); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexName := "users_phone"
|
||||||
|
if usersScoped {
|
||||||
|
indexName = "uk_im_users_tenant_phone"
|
||||||
|
}
|
||||||
|
if exists, err := identityIndexExists(ctx, conn, "im_users", indexName); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
columns := "phone"
|
||||||
|
if usersScoped {
|
||||||
|
columns = "tenant_id,phone"
|
||||||
|
}
|
||||||
|
if _, err = conn.ExecContext(ctx, "CREATE UNIQUE INDEX "+quoteIdent(indexName)+" ON im_users("+columns+")"); err != nil {
|
||||||
|
return fmt.Errorf("create unique phone index: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists, err := identityTableExists(ctx, conn, "im_usertags"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "DROP TABLE im_usertags"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists, err := identityColumnExists(ctx, conn, "im_users", "tags"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if exists {
|
||||||
|
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_users DROP COLUMN tags"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backfillUserPhones(ctx context.Context, conn *sqlx.Conn, scoped bool) error {
|
||||||
|
candidates, err := identityCandidates(ctx, conn, scoped, "tel")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var existing []identityValue
|
||||||
|
prefix := "0 AS tenant_id,"
|
||||||
|
if scoped {
|
||||||
|
prefix = "tenant_id,"
|
||||||
|
}
|
||||||
|
if err = conn.SelectContext(ctx, &existing, "SELECT "+prefix+"id AS user_id,phone AS value FROM im_users WHERE phone IS NOT NULL AND phone<>''"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, item := range existing {
|
||||||
|
seen[identityUniqueKey(item.TenantID, item.Value, scoped)] = true
|
||||||
|
}
|
||||||
|
updatedUsers := make(map[string]bool)
|
||||||
|
for _, item := range candidates {
|
||||||
|
item.Value = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(item.Value, "tel:"), "basic:"))
|
||||||
|
userKey := identityUserKey(item.TenantID, item.UserID, scoped)
|
||||||
|
valueKey := identityUniqueKey(item.TenantID, item.Value, scoped)
|
||||||
|
if item.Value == "" || updatedUsers[userKey] || seen[valueKey] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
query := "UPDATE im_users SET phone=? WHERE id=? AND (phone IS NULL OR phone='')"
|
||||||
|
args := []any{item.Value, item.UserID}
|
||||||
|
if scoped {
|
||||||
|
query = "UPDATE im_users SET phone=? WHERE tenant_id=? AND id=? AND (phone IS NULL OR phone='')"
|
||||||
|
args = []any{item.Value, item.TenantID, item.UserID}
|
||||||
|
}
|
||||||
|
result, err := conn.ExecContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected, _ := result.RowsAffected(); affected > 0 {
|
||||||
|
updatedUsers[userKey] = true
|
||||||
|
seen[valueKey] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backfillProfileEmails(ctx context.Context, conn *sqlx.Conn, usersScoped bool) error {
|
||||||
|
candidates, err := identityCandidates(ctx, conn, usersScoped, "email")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profilesScoped, err := identityColumnExists(ctx, conn, "im_user_profiles", "tenant_id")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if profilesScoped != usersScoped {
|
||||||
|
return errors.New("mysql: im_users and im_user_profiles tenant scope mismatch")
|
||||||
|
}
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
for _, item := range candidates {
|
||||||
|
item.Value = strings.TrimSpace(strings.TrimPrefix(item.Value, "email:"))
|
||||||
|
key := identityUserKey(item.TenantID, item.UserID, usersScoped)
|
||||||
|
if item.Value == "" || processed[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processed[key] = true
|
||||||
|
id, err := nextSnowflakeID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
account := store.EncodeUid(item.UserID).UserId()
|
||||||
|
if usersScoped {
|
||||||
|
_, err = conn.ExecContext(ctx, `INSERT INTO im_user_profiles(id,tenant_id,user_id,email,public_account)
|
||||||
|
VALUES(?,?,?,?,?) ON DUPLICATE KEY UPDATE email=IF(email='',VALUES(email),email)`,
|
||||||
|
id, item.TenantID, item.UserID, item.Value, account)
|
||||||
|
} else {
|
||||||
|
_, err = conn.ExecContext(ctx, `INSERT INTO im_user_profiles(id,user_id,email,public_account)
|
||||||
|
VALUES(?,?,?,?) ON DUPLICATE KEY UPDATE email=IF(email='',VALUES(email),email)`,
|
||||||
|
id, item.UserID, item.Value, account)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityCandidates(ctx context.Context, conn *sqlx.Conn, scoped bool, method string) ([]identityValue, error) {
|
||||||
|
var out []identityValue
|
||||||
|
appendQuery := func(query string, args ...any) error {
|
||||||
|
var rows []identityValue
|
||||||
|
if err := conn.SelectContext(ctx, &rows, query, args...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out = append(out, rows...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
prefix := "0 AS tenant_id,u.id AS user_id,"
|
||||||
|
credentialJoin := "c.userid=u.id"
|
||||||
|
tagJoin := "ut.userid=u.id"
|
||||||
|
if scoped {
|
||||||
|
prefix = "u.tenant_id AS tenant_id,u.id AS user_id,"
|
||||||
|
credentialJoin = "c.tenant_id=u.tenant_id AND c.userid=u.id"
|
||||||
|
tagJoin = "ut.tenant_id=u.tenant_id AND ut.userid=u.id"
|
||||||
|
}
|
||||||
|
if exists, err := identityTableExists(ctx, conn, "im_credentials"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
if err = appendQuery("SELECT "+prefix+"c.value AS value FROM im_users AS u JOIN im_credentials AS c ON "+credentialJoin+
|
||||||
|
" WHERE c.method=? AND c.done=1 AND c.deletedat IS NULL ORDER BY u.id,c.updatedat DESC", method); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if exists, err := identityTableExists(ctx, conn, "im_usertags"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
if err = appendQuery("SELECT "+prefix+"ut.tag AS value FROM im_users AS u JOIN im_usertags AS ut ON "+tagJoin+
|
||||||
|
" WHERE ut.tag LIKE ? ORDER BY u.id,ut.id", method+":%"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if method == "tel" {
|
||||||
|
if exists, err := identityTableExists(ctx, conn, "im_auth"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if exists {
|
||||||
|
authJoin := "a.userid=u.id"
|
||||||
|
if scoped {
|
||||||
|
authJoin = "a.tenant_id=u.tenant_id AND a.userid=u.id"
|
||||||
|
}
|
||||||
|
if err = appendQuery("SELECT " + prefix + "a.uname AS value FROM im_users AS u JOIN im_auth AS a ON " + authJoin +
|
||||||
|
" WHERE a.scheme='basic' AND (a.uname LIKE '+%' OR a.uname LIKE 'basic:+%') ORDER BY u.id"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityTableExists(ctx context.Context, conn *sqlx.Conn, table string) (bool, error) {
|
||||||
|
var exists int
|
||||||
|
err := conn.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM information_schema.tables
|
||||||
|
WHERE table_schema=DATABASE() AND table_name=?)`, table).Scan(&exists)
|
||||||
|
return exists == 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityColumnExists(ctx context.Context, conn *sqlx.Conn, table, column string) (bool, error) {
|
||||||
|
var exists int
|
||||||
|
err := conn.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema=DATABASE() AND table_name=? AND column_name=?)`, table, column).Scan(&exists)
|
||||||
|
return exists == 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityIndexExists(ctx context.Context, conn *sqlx.Conn, table, index string) (bool, error) {
|
||||||
|
var exists int
|
||||||
|
err := conn.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM information_schema.statistics
|
||||||
|
WHERE table_schema=DATABASE() AND table_name=? AND index_name=?)`, table, index).Scan(&exists)
|
||||||
|
return exists == 1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityUserKey(tenantID, userID int64, scoped bool) string {
|
||||||
|
if scoped {
|
||||||
|
return fmt.Sprintf("%d:%d", tenantID, userID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityUniqueKey(tenantID int64, value string, scoped bool) string {
|
||||||
|
if scoped {
|
||||||
|
return fmt.Sprintf("%d:%s", tenantID, value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
+7
-3
@@ -2128,6 +2128,7 @@ func serveIMMeProfile(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
Signature *string `json:"signature"`
|
Signature *string `json:"signature"`
|
||||||
Gender *string `json:"gender"`
|
Gender *string `json:"gender"`
|
||||||
Region *string `json:"region"`
|
Region *string `json:"region"`
|
||||||
|
Email *string `json:"email"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
}
|
}
|
||||||
if !decodeIMJSONBody(wrt, req, &body) {
|
if !decodeIMJSONBody(wrt, req, &body) {
|
||||||
@@ -2146,6 +2147,7 @@ func serveIMMeProfile(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
Signature: body.Signature,
|
Signature: body.Signature,
|
||||||
Gender: body.Gender,
|
Gender: body.Gender,
|
||||||
Region: body.Region,
|
Region: body.Region,
|
||||||
|
Email: body.Email,
|
||||||
Version: body.Version,
|
Version: body.Version,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4860,14 +4862,14 @@ func resolveIMHTTPBearer(req *http.Request) (*imPrincipal, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func currentUserPhone(tenantID types.TenantID, uid types.Uid) (string, error) {
|
func currentUserPhone(tenantID types.TenantID, uid types.Uid) (string, error) {
|
||||||
creds, err := store.Users.GetAllCreds(tenantID, uid, "tel", true)
|
user, err := store.Users.Get(tenantID, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(creds) == 0 || strings.TrimSpace(creds[0].Value) == "" {
|
if user == nil || strings.TrimSpace(user.Phone) == "" {
|
||||||
return "", types.ErrNotFound
|
return "", types.ErrNotFound
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(creds[0].Value), nil
|
return strings.TrimSpace(user.Phone), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSensitiveChallenge(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, action, phone string) {
|
func createSensitiveChallenge(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, action, phone string) {
|
||||||
@@ -6075,6 +6077,7 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
|
|||||||
"signature": profile.Signature,
|
"signature": profile.Signature,
|
||||||
"gender": profile.Gender,
|
"gender": profile.Gender,
|
||||||
"region": profile.Region,
|
"region": profile.Region,
|
||||||
|
"email": profile.Email,
|
||||||
"public_account": profile.PublicAccount,
|
"public_account": profile.PublicAccount,
|
||||||
"version": profile.Version,
|
"version": profile.Version,
|
||||||
"updated_at": profile.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
"updated_at": profile.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||||
@@ -6083,6 +6086,7 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
|
|||||||
"signature": map[string]any{"max_length": 120},
|
"signature": map[string]any{"max_length": 120},
|
||||||
"birth_date": map[string]any{"format": "YYYY-MM-DD"},
|
"birth_date": map[string]any{"format": "YYYY-MM-DD"},
|
||||||
"gender": []string{"male", "female", "other", ""},
|
"gender": []string{"male", "female", "other", ""},
|
||||||
|
"email": map[string]any{"max_length": 128},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -593,6 +593,9 @@ func (s *testProfilesStore) Update(_ types.TenantID, uid types.Uid, patch types.
|
|||||||
if patch.Signature != nil {
|
if patch.Signature != nil {
|
||||||
profile.Signature = *patch.Signature
|
profile.Signature = *patch.Signature
|
||||||
}
|
}
|
||||||
|
if patch.Email != nil {
|
||||||
|
profile.Email = *patch.Email
|
||||||
|
}
|
||||||
profile.Version = patch.Version + 1
|
profile.Version = patch.Version + 1
|
||||||
profile.UpdatedAt = time.Now().UTC().Round(time.Millisecond)
|
profile.UpdatedAt = time.Now().UTC().Round(time.Millisecond)
|
||||||
return profile, nil
|
return profile, nil
|
||||||
@@ -2023,7 +2026,7 @@ func TestServeIMMeProfile(t *testing.T) {
|
|||||||
store.Profiles = profiles
|
store.Profiles = profiles
|
||||||
defer func() { store.Profiles = oldProfiles }()
|
defer func() { store.Profiles = oldProfiles }()
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPatch, "/v1/im/me/profile", strings.NewReader(`{"nickname":"新昵称","signature":"签名","version":1}`))
|
req := httptest.NewRequest(http.MethodPatch, "/v1/im/me/profile", strings.NewReader(`{"nickname":"新昵称","signature":"签名","email":"me@example.com","version":1}`))
|
||||||
req.Header.Set("Authorization", "Bearer token-1")
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
serveIMMeProfile(rec, req)
|
serveIMMeProfile(rec, req)
|
||||||
@@ -2032,7 +2035,7 @@ func TestServeIMMeProfile(t *testing.T) {
|
|||||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
|
||||||
if data["nickname"] != "新昵称" || data["version"].(float64) != 2 {
|
if data["nickname"] != "新昵称" || data["email"] != "me@example.com" || data["version"].(float64) != 2 {
|
||||||
t.Fatalf("unexpected profile: %#v", data)
|
t.Fatalf("unexpected profile: %#v", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1236,24 +1236,25 @@ func TestDispatchAccNew(t *testing.T) {
|
|||||||
|
|
||||||
uid := types.Uid(1)
|
uid := types.Uid(1)
|
||||||
oldSecurity := store.Security
|
oldSecurity := store.Security
|
||||||
|
oldVerifications := store.Verifications
|
||||||
store.Store = ss
|
store.Store = ss
|
||||||
store.Users = uu
|
store.Users = uu
|
||||||
store.Security = &testSecurityStore{version: 1}
|
store.Security = &testSecurityStore{version: 1}
|
||||||
|
store.Verifications = nil
|
||||||
defer func() {
|
defer func() {
|
||||||
store.Store = nil
|
store.Store = nil
|
||||||
store.Users = nil
|
store.Users = nil
|
||||||
store.Security = oldSecurity
|
store.Security = oldSecurity
|
||||||
|
store.Verifications = oldVerifications
|
||||||
ctrl.Finish()
|
ctrl.Finish()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
remoteAddr := "192.168.0.1"
|
remoteAddr := "192.168.0.1"
|
||||||
secret := "<==auth-secret==>"
|
secret := "<==auth-secret==>"
|
||||||
tags := []string{"tag1", "tag2"}
|
|
||||||
authRec := &auth.Rec{
|
authRec := &auth.Rec{
|
||||||
TenantID: 1,
|
TenantID: 1,
|
||||||
Uid: uid,
|
Uid: uid,
|
||||||
AuthLevel: auth.LevelAuth,
|
AuthLevel: auth.LevelAuth,
|
||||||
Tags: tags,
|
|
||||||
State: types.StateOK,
|
State: types.StateOK,
|
||||||
}
|
}
|
||||||
ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa)
|
ss.EXPECT().GetLogicalAuthHandler("basic").Return(aa)
|
||||||
@@ -1271,7 +1272,6 @@ func TestDispatchAccNew(t *testing.T) {
|
|||||||
ss.EXPECT().GetLogicalAuthHandler("token").Return(aa)
|
ss.EXPECT().GetLogicalAuthHandler("token").Return(aa)
|
||||||
token := "<==auth-token==>"
|
token := "<==auth-token==>"
|
||||||
aa.EXPECT().GenSecret(gomock.Any()).Return([]byte(token), time.Now(), nil)
|
aa.EXPECT().GenSecret(gomock.Any()).Return([]byte(token), time.Now(), nil)
|
||||||
uu.EXPECT().UpdateTags(types.TenantID(1), uid, tags, nil, nil).Return(tags, nil)
|
|
||||||
|
|
||||||
s := &Session{
|
s := &Session{
|
||||||
send: make(chan any, 10),
|
send: make(chan any, 10),
|
||||||
@@ -1293,7 +1293,6 @@ func TestDispatchAccNew(t *testing.T) {
|
|||||||
User: "newXYZ",
|
User: "newXYZ",
|
||||||
Scheme: "basic",
|
Scheme: "basic",
|
||||||
Secret: []byte(secret),
|
Secret: []byte(secret),
|
||||||
Tags: []string{"abc", "123"},
|
|
||||||
Desc: &MsgSetDesc{Public: public},
|
Desc: &MsgSetDesc{Public: public},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2034,7 +2034,7 @@ func validUserPrivacyPatch(patch types.UserPrivacyPatch) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
||||||
if patch.AvatarFileID == nil && patch.Nickname == nil && patch.BirthDate == nil && patch.Signature == nil && patch.Gender == nil && patch.Region == nil {
|
if patch.AvatarFileID == nil && patch.Nickname == nil && patch.BirthDate == nil && patch.Signature == nil && patch.Gender == nil && patch.Region == nil && patch.Email == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if patch.Nickname != nil {
|
if patch.Nickname != nil {
|
||||||
@@ -2046,6 +2046,9 @@ func validUserProfilePatch(patch types.UserProfilePatch) bool {
|
|||||||
if patch.Signature != nil && len([]rune(*patch.Signature)) > 120 {
|
if patch.Signature != nil && len([]rune(*patch.Signature)) > 120 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if patch.Email != nil && len([]rune(strings.TrimSpace(*patch.Email))) > 128 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type UserProfile struct {
|
|||||||
Signature string
|
Signature string
|
||||||
Gender string
|
Gender string
|
||||||
Region string
|
Region string
|
||||||
|
Email string
|
||||||
PublicAccount string
|
PublicAccount string
|
||||||
Version int
|
Version int
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@@ -23,6 +24,7 @@ type UserProfilePatch struct {
|
|||||||
Signature *string
|
Signature *string
|
||||||
Gender *string
|
Gender *string
|
||||||
Region *string
|
Region *string
|
||||||
|
Email *string
|
||||||
Version int
|
Version int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -519,6 +519,8 @@ type User struct {
|
|||||||
LastSeen *time.Time
|
LastSeen *time.Time
|
||||||
// User agent provided when accessing the topic last time
|
// User agent provided when accessing the topic last time
|
||||||
UserAgent string
|
UserAgent string
|
||||||
|
// Canonical mobile number used for registration and account recovery.
|
||||||
|
Phone string
|
||||||
|
|
||||||
Public any
|
Public any
|
||||||
Trusted any
|
Trusted any
|
||||||
|
|||||||
+3
-3
@@ -45,7 +45,7 @@
|
|||||||
"auth_config": {
|
"auth_config": {
|
||||||
"logical_names": [],
|
"logical_names": [],
|
||||||
"basic": {
|
"basic": {
|
||||||
"add_to_tags": true,
|
"add_to_tags": false,
|
||||||
"min_login_length": 4,
|
"min_login_length": 4,
|
||||||
"min_password_length": 6
|
"min_password_length": 6
|
||||||
},
|
},
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
"acc_validation": {
|
"acc_validation": {
|
||||||
"email": {
|
"email": {
|
||||||
"add_to_tags": true,
|
"add_to_tags": false,
|
||||||
"required": [],
|
"required": [],
|
||||||
"config": {
|
"config": {
|
||||||
"host_url": "http://64.118.137.182:6060/",
|
"host_url": "http://64.118.137.182:6060/",
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tel": {
|
"tel": {
|
||||||
"add_to_tags": true,
|
"add_to_tags": false,
|
||||||
"required": [],
|
"required": [],
|
||||||
"config": {
|
"config": {
|
||||||
"host_url": "http://64.118.137.182:6060/",
|
"host_url": "http://64.118.137.182:6060/",
|
||||||
|
|||||||
+12
-1
@@ -3071,6 +3071,10 @@ func (t *Topic) replySetTags(sess *Session, asUid types.Uid, msg *ClientComMessa
|
|||||||
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
||||||
return errors.New("invalid topic category to assign tags")
|
return errors.New("invalid topic category to assign tags")
|
||||||
}
|
}
|
||||||
|
if t.cat == types.TopicCatMe {
|
||||||
|
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
||||||
|
return errors.New("user tags are no longer supported")
|
||||||
|
}
|
||||||
|
|
||||||
if t.cat == types.TopicCatGrp && t.owner != asUid {
|
if t.cat == types.TopicCatGrp && t.owner != asUid {
|
||||||
sess.queueOut(ErrPermissionDeniedReply(msg, now))
|
sess.queueOut(ErrPermissionDeniedReply(msg, now))
|
||||||
@@ -3168,7 +3172,6 @@ func (t *Topic) replyGetCreds(sess *Session, asUid types.Uid, msg *ClientComMess
|
|||||||
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
||||||
return errors.New("invalid topic category for getting credentials")
|
return errors.New("invalid topic category for getting credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
screds, err := store.Users.GetAllCreds(sess.tenantID, asUid, "", false)
|
screds, err := store.Users.GetAllCreds(sess.tenantID, asUid, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.queueOut(decodeStoreErrorExplicitTs(err, id, msg.Original, now, msg.Timestamp, nil))
|
sess.queueOut(decodeStoreErrorExplicitTs(err, id, msg.Original, now, msg.Timestamp, nil))
|
||||||
@@ -3206,6 +3209,10 @@ func (t *Topic) replySetCred(sess *Session, asUid types.Uid, authLevel auth.Leve
|
|||||||
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
||||||
return errors.New("invalid topic category for updating credentials")
|
return errors.New("invalid topic category for updating credentials")
|
||||||
}
|
}
|
||||||
|
if set.Cred.Method == "tel" || set.Cred.Method == "email" {
|
||||||
|
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
||||||
|
return errors.New("phone and email are managed by product profile APIs")
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var tags []string
|
var tags []string
|
||||||
@@ -3484,6 +3491,10 @@ func (t *Topic) replyDelCred(sess *Session, asUid types.Uid, authLvl auth.Level,
|
|||||||
sess.queueOut(ErrMalformedReply(msg, now))
|
sess.queueOut(ErrMalformedReply(msg, now))
|
||||||
return errors.New("del.cred: missing method")
|
return errors.New("del.cred: missing method")
|
||||||
}
|
}
|
||||||
|
if del.Cred.Method == "tel" || del.Cred.Method == "email" {
|
||||||
|
sess.queueOut(ErrOperationNotAllowedReply(msg, now))
|
||||||
|
return errors.New("phone and email are managed by product profile APIs")
|
||||||
|
}
|
||||||
|
|
||||||
tags, err := deleteCred(sess.tenantID, asUid, authLvl, del.Cred)
|
tags, err := deleteCred(sess.tenantID, asUid, authLvl, del.Cred)
|
||||||
if tags != nil {
|
if tags != nil {
|
||||||
|
|||||||
+40
-85
@@ -73,31 +73,47 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
|||||||
user.State = state
|
user.State = state
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure tags are unique and not restricted.
|
if msg.Acc.Tags != nil {
|
||||||
if tags := normalizeTags(msg.Acc.Tags, globals.maxTagCount); tags != nil {
|
logs.Warn.Println("create user: user tags are no longer supported, sid=", s.sid)
|
||||||
if !restrictedTagsEqual(tags, nil, globals.immutableTagNS) {
|
msg := ErrPermissionDenied(msg.Id, "", msg.Timestamp)
|
||||||
logs.Warn.Println("create user: attempt to directly assign restricted tags, sid=", s.sid)
|
msg.Ctrl.Params = map[string]any{"what": "tags"}
|
||||||
msg := ErrPermissionDenied(msg.Id, "", msg.Timestamp)
|
s.queueOut(msg)
|
||||||
msg.Ctrl.Params = map[string]any{"what": "tags"}
|
return
|
||||||
s.queueOut(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user.Tags = tags
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-check credentials for validity. We don't know user's access level
|
// Pre-check credentials for validity. We don't know user's access level
|
||||||
// consequently cannot check presence of required credentials. Must do that later.
|
// consequently cannot check presence of required credentials. Must do that later.
|
||||||
creds := normalizeCredentials(msg.Acc.Cred, true)
|
creds := normalizeCredentials(msg.Acc.Cred, true)
|
||||||
|
var registrationPhone string
|
||||||
for i := range creds {
|
for i := range creds {
|
||||||
cr := &creds[i]
|
cr := &creds[i]
|
||||||
vld := store.Store.GetValidator(cr.Method)
|
vld := store.Store.GetValidator(cr.Method)
|
||||||
if _, err := vld.PreCheck(cr.Value, cr.Params); err != nil {
|
normalized, err := vld.PreCheck(cr.Value, cr.Params)
|
||||||
|
if err != nil {
|
||||||
logs.Warn.Println("create user: failed credential pre-check", cr, err, "sid=", s.sid)
|
logs.Warn.Println("create user: failed credential pre-check", cr, err, "sid=", s.sid)
|
||||||
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp,
|
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp,
|
||||||
map[string]any{"what": cr.Method}))
|
map[string]any{"what": cr.Method}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if cr.Method == "tel" {
|
||||||
|
phone := strings.TrimSpace(strings.TrimPrefix(normalized, "tel:"))
|
||||||
|
challengeID, _ := cr.Params["challenge_id"].(string)
|
||||||
|
if phone == "" || strings.TrimSpace(challengeID) == "" || strings.TrimSpace(cr.Response) == "" ||
|
||||||
|
(registrationPhone != "" && registrationPhone != phone) {
|
||||||
|
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||||
|
map[string]any{"creds": []string{"tel"}}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registrationPhone = phone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if registrationPhone == "" && store.Verifications != nil {
|
||||||
|
logs.Warn.Println("create user: verified phone is required, sid=", s.sid)
|
||||||
|
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
|
||||||
|
map[string]any{"creds": []string{"tel"}}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Phone = registrationPhone
|
||||||
|
|
||||||
// Assign default access values in case the acc creator has not provided them
|
// Assign default access values in case the acc creator has not provided them
|
||||||
user.Access.Auth = getDefaultAccess(types.TopicCatP2P, true, false) |
|
user.Access.Auth = getDefaultAccess(types.TopicCatP2P, true, false) |
|
||||||
@@ -138,9 +154,9 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add authentication record. The authhdl.AddRecord may change tags.
|
// Add authentication record.
|
||||||
rec, err := authhdl.AddRecord(authCtx,
|
rec, err := authhdl.AddRecord(authCtx,
|
||||||
&auth.Rec{TenantID: s.tenantID, Uid: user.Uid(), Tags: user.Tags}, msg.Acc.Secret)
|
&auth.Rec{TenantID: s.tenantID, Uid: user.Uid()}, msg.Acc.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logs.Warn.Println("create user: add auth record failed", err, "sid=", s.sid)
|
logs.Warn.Println("create user: add auth record failed", err, "sid=", s.sid)
|
||||||
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
|
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
|
||||||
@@ -167,14 +183,14 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save credentials, update tags if necessary.
|
// Save and validate credentials.
|
||||||
tmpToken, _, _ := store.Store.GetLogicalAuthHandler("token").GenSecret(&auth.Rec{
|
tmpToken, _, _ := store.Store.GetLogicalAuthHandler("token").GenSecret(&auth.Rec{
|
||||||
TenantID: s.tenantID,
|
TenantID: s.tenantID,
|
||||||
Uid: user.Uid(),
|
Uid: user.Uid(),
|
||||||
AuthLevel: auth.LevelAuth,
|
AuthLevel: auth.LevelAuth,
|
||||||
Lifetime: auth.Duration(time.Hour * 24),
|
Lifetime: auth.Duration(time.Hour * 24),
|
||||||
})
|
})
|
||||||
validated, _, err := addCreds(s.tenantID, user.Uid(), creds, rec.Tags, s.lang, tmpToken)
|
validated, _, err := addCreds(s.tenantID, user.Uid(), creds, nil, s.lang, tmpToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logs.Warn.Println("create user: failed to save or validate credential", err, "sid=", s.sid)
|
logs.Warn.Println("create user: failed to save or validate credential", err, "sid=", s.sid)
|
||||||
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
|
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
|
||||||
@@ -351,17 +367,11 @@ func updateUserAuth(tenantID types.TenantID, msg *ClientComMessage, user *types.
|
|||||||
|
|
||||||
// TODO(gene): support adding new auth schemes
|
// TODO(gene): support adding new auth schemes
|
||||||
|
|
||||||
rec, err := authhdl.UpdateRecord(auth.AuthContext{TenantID: tenantID, RemoteAddr: remoteAddr},
|
_, err := authhdl.UpdateRecord(auth.AuthContext{TenantID: tenantID, RemoteAddr: remoteAddr},
|
||||||
&auth.Rec{TenantID: tenantID, Uid: user.Uid(), Tags: user.Tags}, msg.Acc.Secret)
|
&auth.Rec{TenantID: tenantID, Uid: user.Uid()}, msg.Acc.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags may have been changed by authhdl.UpdateRecord, reset them.
|
|
||||||
// Can't do much with the error here, logging it but not returning.
|
|
||||||
if _, err = store.Users.UpdateTags(tenantID, user.Uid(), nil, nil, rec.Tags); err != nil {
|
|
||||||
logs.Warn.Println("updateUserAuth tags update failed:", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,11 +379,8 @@ func updateUserAuth(tenantID types.TenantID, msg *ClientComMessage, user *types.
|
|||||||
return types.ErrMalformed
|
return types.ErrMalformed
|
||||||
}
|
}
|
||||||
|
|
||||||
// addCreds adds new credentials and re-send validation request for existing ones.
|
// addCreds adds credentials and re-sends validation requests for existing ones.
|
||||||
// It also adds credential-defined tags if necessary.
|
func addCreds(tenantID types.TenantID, uid types.Uid, creds []MsgCredClient, _ []string,
|
||||||
// Returns methods validated in this call only. Returns either a full set of tags
|
|
||||||
// or nil for tags when tags are unchanged.
|
|
||||||
func addCreds(tenantID types.TenantID, uid types.Uid, creds []MsgCredClient, extraTags []string,
|
|
||||||
lang string, tmpToken []byte) ([]string, []string, error) {
|
lang string, tmpToken []byte) ([]string, []string, error) {
|
||||||
var validated []string
|
var validated []string
|
||||||
for i := range creds {
|
for i := range creds {
|
||||||
@@ -385,14 +392,11 @@ func addCreds(tenantID types.TenantID, uid types.Uid, creds []MsgCredClient, ext
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cr.Response != "" {
|
if cr.Response != "" {
|
||||||
if value, ok, err := consumeProductVerificationCred(tenantID, uid, cr, vld.PreCheck); ok || err != nil {
|
if _, ok, err := consumeProductVerificationCred(tenantID, uid, cr, vld.PreCheck); ok || err != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
validated = append(validated, cr.Method)
|
validated = append(validated, cr.Method)
|
||||||
if globals.validators[cr.Method].addToTags {
|
|
||||||
extraTags = append(extraTags, cr.Method+":"+value)
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,24 +411,9 @@ func addCreds(tenantID types.TenantID, uid types.Uid, creds []MsgCredClient, ext
|
|||||||
// successfully validated.
|
// successfully validated.
|
||||||
validated = append(validated, cr.Method)
|
validated = append(validated, cr.Method)
|
||||||
|
|
||||||
// Generate tags for these confirmed credentials.
|
|
||||||
if globals.validators[cr.Method].addToTags {
|
|
||||||
extraTags = append(extraTags, cr.Method+":"+cr.Value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return validated, nil, nil
|
||||||
// Save tags potentially changed by the validator.
|
|
||||||
if len(extraTags) > 0 {
|
|
||||||
if utags, err := store.Users.UpdateTags(tenantID, uid, extraTags, nil, nil); err == nil {
|
|
||||||
extraTags = utags
|
|
||||||
} else {
|
|
||||||
logs.Warn.Println("add cred tags update failed:", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
extraTags = nil
|
|
||||||
}
|
|
||||||
return validated, extraTags, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func consumeProductVerificationCred(tenantID types.TenantID, uid types.Uid, cr *MsgCredClient,
|
func consumeProductVerificationCred(tenantID types.TenantID, uid types.Uid, cr *MsgCredClient,
|
||||||
@@ -475,7 +464,6 @@ func consumeProductVerificationCred(tenantID types.TenantID, uid types.Uid, cr *
|
|||||||
|
|
||||||
// validatedCreds returns the list of validated credentials including those validated in this call.
|
// validatedCreds returns the list of validated credentials including those validated in this call.
|
||||||
// Returns all validated methods including those validated earlier and now.
|
// Returns all validated methods including those validated earlier and now.
|
||||||
// Returns either a full set of tags or nil for tags if tags are unchanged.
|
|
||||||
func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, creds []MsgCredClient,
|
func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, creds []MsgCredClient,
|
||||||
errorOnFail bool) ([]string, []string, error) {
|
errorOnFail bool) ([]string, []string, error) {
|
||||||
// Check if credential validation is required.
|
// Check if credential validation is required.
|
||||||
@@ -497,7 +485,6 @@ func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level,
|
|||||||
// Add credentials which are validated in this call.
|
// Add credentials which are validated in this call.
|
||||||
// Unknown validators are removed.
|
// Unknown validators are removed.
|
||||||
creds = normalizeCredentials(creds, false)
|
creds = normalizeCredentials(creds, false)
|
||||||
var tagsToAdd []string
|
|
||||||
for i := range creds {
|
for i := range creds {
|
||||||
cr := &creds[i]
|
cr := &creds[i]
|
||||||
if cr.Response == "" {
|
if cr.Response == "" {
|
||||||
@@ -506,7 +493,7 @@ func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level,
|
|||||||
}
|
}
|
||||||
|
|
||||||
vld := store.Store.GetValidator(cr.Method) // No need to check for nil, unknown methods are removed earlier.
|
vld := store.Store.GetValidator(cr.Method) // No need to check for nil, unknown methods are removed earlier.
|
||||||
value, err := vld.Check(tenantID, uid, cr.Response)
|
_, err := vld.Check(tenantID, uid, cr.Response)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check failed.
|
// Check failed.
|
||||||
@@ -525,23 +512,6 @@ func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level,
|
|||||||
// Check did not return an error: the request was successfully validated.
|
// Check did not return an error: the request was successfully validated.
|
||||||
methods[cr.Method] = struct{}{}
|
methods[cr.Method] = struct{}{}
|
||||||
|
|
||||||
// Add validated credential to user's tags.
|
|
||||||
if globals.validators[cr.Method].addToTags {
|
|
||||||
tagsToAdd = append(tagsToAdd, cr.Method+":"+value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tags []string
|
|
||||||
if len(tagsToAdd) > 0 {
|
|
||||||
// Save update to tags
|
|
||||||
if utags, err := store.Users.UpdateTags(tenantID, uid, tagsToAdd, nil, nil); err == nil {
|
|
||||||
tags = utags
|
|
||||||
} else {
|
|
||||||
logs.Warn.Println("validated creds tags update failed:", err)
|
|
||||||
tags = nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tags = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validated := make([]string, 0, len(methods))
|
validated := make([]string, 0, len(methods))
|
||||||
@@ -549,11 +519,10 @@ func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level,
|
|||||||
validated = append(validated, method)
|
validated = append(validated, method)
|
||||||
}
|
}
|
||||||
|
|
||||||
return validated, tags, nil
|
return validated, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteCred deletes user's credential.
|
// deleteCred deletes user's credential.
|
||||||
// Returns full set of remaining tags or nil if tags are unchanged.
|
|
||||||
func deleteCred(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, cred *MsgCredClient) ([]string, error) {
|
func deleteCred(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, cred *MsgCredClient) ([]string, error) {
|
||||||
vld := store.Store.GetValidator(cred.Method)
|
vld := store.Store.GetValidator(cred.Method)
|
||||||
if vld == nil || cred.Value == "" {
|
if vld == nil || cred.Value == "" {
|
||||||
@@ -601,21 +570,7 @@ func deleteCred(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, cred
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove generated tags for the deleted credential.
|
return nil, nil
|
||||||
var tags []string
|
|
||||||
if globals.validators[cred.Method].addToTags {
|
|
||||||
// This error should not be returned to user.
|
|
||||||
if utags, err := store.Users.UpdateTags(tenantID, uid, nil, []string{cred.Method + ":" + cred.Value}, nil); err == nil {
|
|
||||||
tags = utags
|
|
||||||
} else {
|
|
||||||
logs.Warn.Println("delete cred: failed to update tags:", err)
|
|
||||||
tags = nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tags = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change user state: suspended/normal (ok).
|
// Change user state: suspended/normal (ok).
|
||||||
|
|||||||
Reference in New Issue
Block a user