feat: add im home and message action flows
This commit is contained in:
@@ -274,6 +274,10 @@ type TenantBusinessAdapter interface {
|
||||
TenantFindOne(t.TenantID, string) (string, error)
|
||||
|
||||
TenantMessageSave(t.TenantID, *t.Message) error
|
||||
TenantMessageGetByClientID(t.TenantID, t.Uid, string) (*t.Message, error)
|
||||
TenantMessageGetOne(t.TenantID, string, int) (*t.Message, error)
|
||||
TenantMessageRecall(t.TenantID, string, int, t.Uid, int, time.Time) (*t.Message, error)
|
||||
TenantMessageEdit(t.TenantID, string, int, t.Uid, int, t.KVMap, any, string, time.Time) (*t.Message, error)
|
||||
TenantMessageGetAll(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.Message, error)
|
||||
TenantMessageDeleteList(t.TenantID, string, *t.DelMessage) error
|
||||
TenantMessageGetDeleted(t.TenantID, string, t.Uid, *t.QueryOpt) ([]t.DelMessage, error)
|
||||
@@ -302,4 +306,15 @@ type TenantBusinessAdapter interface {
|
||||
|
||||
TenantUserSecurityGetAuthVersion(t.TenantID, t.Uid) (int, error)
|
||||
TenantUserSecurityBumpAuthVersion(t.TenantID, t.Uid, time.Time) (int, error)
|
||||
TenantRealNameGet(t.TenantID, t.Uid) (*t.RealNameVerification, error)
|
||||
TenantRealNameCreate(t.TenantID, *t.RealNameVerification) error
|
||||
TenantActionTokenCreate(t.TenantID, *t.ActionToken) error
|
||||
TenantActionTokenConsume(t.TenantID, t.Uid, string, string, string, time.Time) (*t.ActionToken, error)
|
||||
TenantIndependentPasswordGet(t.TenantID, t.Uid, string) (*t.IndependentPassword, error)
|
||||
TenantIndependentPasswordUpsert(t.TenantID, *t.IndependentPassword) error
|
||||
TenantIndependentPasswordUpdateAttempts(t.TenantID, t.Uid, string, int, *time.Time, time.Time) error
|
||||
|
||||
TenantSystemNotificationCreate(t.TenantID, *t.SystemNotification) error
|
||||
TenantSystemNotificationList(t.TenantID, t.Uid, string, int) ([]t.SystemNotification, string, error)
|
||||
TenantSystemNotificationMarkRead(t.TenantID, t.Uid, string, time.Time) error
|
||||
}
|
||||
|
||||
+251
-11
@@ -41,7 +41,7 @@ type adapter struct {
|
||||
}
|
||||
|
||||
const (
|
||||
adpVersion = 118
|
||||
adpVersion = 122
|
||||
adapterName = "mysql"
|
||||
|
||||
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true"
|
||||
@@ -56,6 +56,8 @@ const (
|
||||
txTimeoutMultiplier = 1.5
|
||||
)
|
||||
|
||||
const defaultTenantID = t.TenantID(1)
|
||||
|
||||
type configType struct {
|
||||
// DB connection settings.
|
||||
// See https://pkg.go.dev/github.com/go-sql-driver/mysql#Config
|
||||
@@ -381,6 +383,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
tenant_id BIGINT NOT NULL,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
@@ -412,6 +415,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE users(
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
@@ -423,6 +427,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
trusted JSON,
|
||||
tags JSON,
|
||||
PRIMARY KEY(id),
|
||||
INDEX users_tenant_id_id(tenant_id, id),
|
||||
INDEX users_state_stateat(state, stateat),
|
||||
INDEX users_lastseen_updatedat(lastseen, updatedat)
|
||||
)`); err != nil {
|
||||
@@ -443,15 +448,24 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = createIMSecurityTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMNotificationTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Indexed user tags.
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE usertags(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
userid BIGINT NOT NULL,
|
||||
tag VARCHAR(96) NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX usertags_tag(tag),
|
||||
INDEX usertags_tenant_tag(tenant_id, tag),
|
||||
UNIQUE INDEX usertags_userid_tag(userid, tag)
|
||||
)`); err != nil {
|
||||
return err
|
||||
@@ -461,6 +475,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE devices(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
userid BIGINT NOT NULL,
|
||||
hash CHAR(16) NOT NULL,
|
||||
deviceid TEXT NOT NULL,
|
||||
@@ -469,6 +484,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
lang VARCHAR(8),
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX devices_tenant_userid(tenant_id, userid),
|
||||
UNIQUE INDEX devices_hash(hash)
|
||||
)`); err != nil {
|
||||
return err
|
||||
@@ -478,6 +494,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE auth(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
uname VARCHAR(32) NOT NULL,
|
||||
userid BIGINT NOT NULL,
|
||||
scheme VARCHAR(16) NOT NULL,
|
||||
@@ -486,8 +503,8 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
expires DATETIME,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE INDEX auth_userid_scheme(userid, scheme),
|
||||
UNIQUE INDEX auth_uname(uname)
|
||||
UNIQUE INDEX auth_tenant_uname(tenant_id, uname),
|
||||
UNIQUE INDEX auth_userid_scheme(userid, scheme)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -496,6 +513,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE topics(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
@@ -513,8 +531,10 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
tags JSON,
|
||||
aux JSON,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX topics_tenant_name(tenant_id, name),
|
||||
UNIQUE INDEX topics_name(name),
|
||||
INDEX topics_owner(owner),
|
||||
INDEX topics_tenant_owner(tenant_id, owner),
|
||||
INDEX topics_state_stateat(state, stateat),
|
||||
INDEX topics_name_state_seqid(name, state, seqid)
|
||||
)`); err != nil {
|
||||
@@ -530,11 +550,13 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE topictags(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
topic CHAR(25) NOT NULL,
|
||||
tag VARCHAR(96) NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX topictags_tag(tag),
|
||||
INDEX topictags_tenant_tag(tenant_id, tag),
|
||||
UNIQUE INDEX topictags_topic_tag(topic, tag)
|
||||
)`); err != nil {
|
||||
return err
|
||||
@@ -544,6 +566,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE subscriptions(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
@@ -557,6 +580,8 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
private JSON,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX subscriptions_tenant_userid(tenant_id, userid),
|
||||
INDEX subscriptions_tenant_topic(tenant_id, topic),
|
||||
UNIQUE INDEX subscriptions_topic_userid(topic, userid),
|
||||
INDEX subscriptions_topic(topic),
|
||||
INDEX subscriptions_deletedat(deletedat),
|
||||
@@ -569,6 +594,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE messages(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
@@ -576,19 +602,46 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
seqid INT NOT NULL,
|
||||
topic CHAR(25) NOT NULL,` +
|
||||
"`from` BIGINT NOT NULL," +
|
||||
`head JSON,
|
||||
`client_msg_id VARCHAR(64),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
recalled_at DATETIME(3),
|
||||
recalled_by BIGINT,
|
||||
head JSON,
|
||||
content JSON,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX messages_tenant_topic(tenant_id, topic),
|
||||
UNIQUE INDEX messages_sender_client_mid(tenant_id, ` + "`from`" + `, client_msg_id),
|
||||
UNIQUE INDEX messages_topic_seqid(topic, seqid)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE im_message_revisions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
|
||||
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deletion log
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE dellog(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
topic CHAR(25) NOT NULL,
|
||||
deletedfor BIGINT NOT NULL DEFAULT 0,
|
||||
delid INT NOT NULL,
|
||||
@@ -596,6 +649,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
hi INT NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX dellog_tenant_topic(tenant_id, topic),
|
||||
INDEX dellog_topic_delid_deletedfor(topic,delid,deletedfor),
|
||||
INDEX dellog_topic_deletedfor_low_hi(topic,deletedfor,low,hi),
|
||||
INDEX dellog_deletedfor(deletedfor)
|
||||
@@ -607,6 +661,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE credentials(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
@@ -619,7 +674,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
retries INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE credentials_uniqueness(synthetic)
|
||||
UNIQUE credentials_tenant_uniqueness(tenant_id, synthetic)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -629,6 +684,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE fileuploads(
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
userid BIGINT,
|
||||
@@ -638,6 +694,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
etag VARCHAR(128),
|
||||
location VARCHAR(2048) NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
INDEX fileuploads_tenant_status(tenant_id, status),
|
||||
INDEX fileuploads_status(status)
|
||||
)`); err != nil {
|
||||
return err
|
||||
@@ -647,12 +704,14 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE filemsglinks(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
fileid BIGINT NOT NULL,
|
||||
msgid INT,
|
||||
topic CHAR(25),
|
||||
userid BIGINT,
|
||||
PRIMARY KEY(id),
|
||||
INDEX filemsglinks_tenant_fileid(tenant_id, fileid),
|
||||
FOREIGN KEY(fileid) REFERENCES fileuploads(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(topic) REFERENCES topics(name) ON DELETE CASCADE,
|
||||
@@ -984,6 +1043,7 @@ func (a *adapter) UpgradeDb() error {
|
||||
tenant_id BIGINT NOT NULL,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
@@ -1031,6 +1091,64 @@ func (a *adapter) UpgradeDb() error {
|
||||
}
|
||||
}
|
||||
|
||||
if a.version == 118 {
|
||||
if _, err := a.db.Exec("ALTER TABLE im_verification_codes ADD COLUMN action VARCHAR(64) NOT NULL DEFAULT '' AFTER scene"); err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := a.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMSecurityTables(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bumpVersion(a, 119); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.version == 119 {
|
||||
tx, err := a.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = createIMNotificationTables(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bumpVersion(a, 120); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.version == 120 {
|
||||
if _, err := a.db.Exec("ALTER TABLE messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := a.db.Exec("CREATE UNIQUE INDEX messages_sender_client_mid ON messages(tenant_id, `from`, client_msg_id)"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bumpVersion(a, 121); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.version == 121 {
|
||||
if err := upgradeIMMessageActions(a); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bumpVersion(a, 122); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.version != adpVersion {
|
||||
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
|
||||
". DB is still at " + strconv.Itoa(a.version))
|
||||
@@ -1038,6 +1156,121 @@ func (a *adapter) UpgradeDb() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createIMNotificationTables(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(
|
||||
`CREATE TABLE im_system_notifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event(tenant_id, user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox(tenant_id, user_id, id DESC),
|
||||
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func upgradeIMMessageActions(a *adapter) error {
|
||||
statements := []string{
|
||||
"ALTER TABLE messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id",
|
||||
"ALTER TABLE messages ADD COLUMN recalled_at DATETIME(3) AFTER version",
|
||||
"ALTER TABLE messages ADD COLUMN recalled_by BIGINT AFTER recalled_at",
|
||||
`CREATE TABLE im_message_revisions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
|
||||
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := a.db.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createIMSecurityTables(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_realname_verifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_realname_submission(tenant_id, submission_id),
|
||||
INDEX idx_im_realname_user_latest(tenant_id, user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash(tenant_id, id_number_hash),
|
||||
CONSTRAINT fk_im_realname_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
`CREATE TABLE im_action_tokens(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_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),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token(tenant_id, token_id),
|
||||
INDEX idx_im_action_tokens_user_action(tenant_id, user_id, action, target, state, expires_at),
|
||||
CONSTRAINT fk_im_action_tokens_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Exec(
|
||||
`CREATE TABLE im_youth_mode_settings(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until 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),
|
||||
PRIMARY KEY(tenant_id, user_id, purpose),
|
||||
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
)`)
|
||||
return err
|
||||
}
|
||||
|
||||
// Create system topic 'sys'.
|
||||
func createSystemTopic(tx *sql.Tx) error {
|
||||
now := t.TimeNow()
|
||||
@@ -1117,9 +1350,12 @@ func removeTags(tx *sqlx.Tx, table, keyName string, keyVal any, tags []string) e
|
||||
// UserCreate creates a new user. Returns error and true if error is due to duplicate user name,
|
||||
// false for any other error
|
||||
func (a *adapter) UserCreate(user *t.User) error {
|
||||
if user == nil || user.TenantID.IsZero() {
|
||||
if user == nil {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if user.TenantID.IsZero() {
|
||||
user.TenantID = defaultTenantID
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
@@ -1955,9 +2191,13 @@ func (a *adapter) TopicCreate(topic *t.Topic) error {
|
||||
|
||||
// If undelete = true - update subscription on duplicate key, otherwise ignore the duplicate.
|
||||
func createSubscription(tx *sqlx.Tx, sub *t.Subscription, undelete bool) error {
|
||||
if sub == nil || sub.TenantID.IsZero() {
|
||||
if sub == nil {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
tenantID := sub.TenantID
|
||||
if tenantID.IsZero() {
|
||||
tenantID = defaultTenantID
|
||||
}
|
||||
|
||||
isOwner := (sub.ModeGiven & sub.ModeWant).IsOwner()
|
||||
|
||||
@@ -1966,26 +2206,26 @@ func createSubscription(tx *sqlx.Tx, sub *t.Subscription, undelete bool) error {
|
||||
_, err := tx.Exec(
|
||||
"INSERT INTO subscriptions(tenant_id,createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
|
||||
"VALUES(?,?,?,NULL,?,?,?,?,?)",
|
||||
sub.TenantID, sub.CreatedAt, sub.UpdatedAt, decoded_uid, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
|
||||
tenantID, sub.CreatedAt, sub.UpdatedAt, decoded_uid, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
|
||||
|
||||
if err != nil && isDupe(err) {
|
||||
if undelete {
|
||||
_, err = tx.Exec("UPDATE subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
"delid=0,recvseqid=0,readseqid=0 WHERE tenant_id=? AND topic=? AND userid=?",
|
||||
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), sub.TenantID,
|
||||
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), tenantID,
|
||||
sub.Topic, decoded_uid)
|
||||
} else {
|
||||
_, err = tx.Exec("UPDATE subscriptions SET createdat=?,updatedat=?,deletedat=NULL,modeWant=?,modeGiven=?,"+
|
||||
"delid=0,recvseqid=0,readseqid=0,private=? WHERE tenant_id=? AND topic=? AND userid=?",
|
||||
sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv,
|
||||
sub.TenantID, sub.Topic, decoded_uid)
|
||||
tenantID, sub.Topic, decoded_uid)
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && isOwner {
|
||||
// Update topic owner if the subscription is with owner rights.
|
||||
// Don't increment subscriber count here - it's done in TopicShare in bulk.
|
||||
_, err = tx.Exec("UPDATE topics SET owner=? WHERE tenant_id=? AND name=?", decoded_uid, sub.TenantID, sub.Topic)
|
||||
_, err = tx.Exec("UPDATE topics SET owner=? WHERE tenant_id=? AND name=?", decoded_uid, tenantID, sub.Topic)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+132
-7
@@ -50,6 +50,7 @@ CREATE TABLE im_verification_codes(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
@@ -83,10 +84,11 @@ CREATE TABLE kvmeta(
|
||||
INDEX kvmeta_createdat_key(createdat, `key`)
|
||||
);
|
||||
|
||||
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "118");
|
||||
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "122");
|
||||
|
||||
CREATE TABLE users(
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
@@ -95,9 +97,11 @@ CREATE TABLE users(
|
||||
lastseen DATETIME,
|
||||
useragent VARCHAR(255) DEFAULT '',
|
||||
public JSON,
|
||||
trusted JSON,
|
||||
tags JSON, -- Denormalized array of tags
|
||||
|
||||
PRIMARY KEY(id),
|
||||
INDEX users_tenant_id_id(tenant_id, id),
|
||||
INDEX users_state_stateat(state, stateat),
|
||||
INDEX users_lastseen_updatedat(lastseen, updatedat)
|
||||
);
|
||||
@@ -114,21 +118,101 @@ CREATE TABLE im_user_security(
|
||||
CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE im_realname_verifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_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),
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_realname_submission(tenant_id, submission_id),
|
||||
INDEX idx_im_realname_user_latest(tenant_id, user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash(tenant_id, id_number_hash),
|
||||
CONSTRAINT fk_im_realname_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE im_action_tokens(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_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),
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token(tenant_id, token_id),
|
||||
INDEX idx_im_action_tokens_user_action(tenant_id, user_id, action, target, state, expires_at),
|
||||
CONSTRAINT fk_im_action_tokens_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE im_youth_mode_settings(
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until 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),
|
||||
|
||||
PRIMARY KEY(tenant_id, user_id, purpose),
|
||||
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE im_system_notifications(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_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),
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event(tenant_id, user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox(tenant_id, user_id, id DESC),
|
||||
CONSTRAINT fk_im_system_notifications_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
# Indexed user tags.
|
||||
CREATE TABLE usertags(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
userid BIGINT NOT NULL,
|
||||
tag VARCHAR(96) NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX usertags_tag(tag),
|
||||
INDEX usertags_tenant_tag(tenant_id, tag),
|
||||
UNIQUE INDEX usertags_userid_tag(userid, tag)
|
||||
);
|
||||
|
||||
# Indexed devices. Normalized into a separate table.
|
||||
CREATE TABLE devices(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
userid BIGINT NOT NULL,
|
||||
hash CHAR(16) NOT NULL,
|
||||
deviceid TEXT NOT NULL,
|
||||
@@ -138,12 +222,14 @@ CREATE TABLE devices(
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX devices_tenant_userid(tenant_id, userid),
|
||||
UNIQUE INDEX devices_hash(hash)
|
||||
);
|
||||
|
||||
# Authentication records for the basic authentication scheme.
|
||||
CREATE TABLE auth(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
uname VARCHAR(32) NOT NULL,
|
||||
userid BIGINT NOT NULL,
|
||||
scheme VARCHAR(16) NOT NULL,
|
||||
@@ -153,14 +239,15 @@ CREATE TABLE auth(
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE INDEX auth_userid_scheme(userid, scheme),
|
||||
UNIQUE INDEX auth_uname (uname)
|
||||
UNIQUE INDEX auth_tenant_uname(tenant_id, uname),
|
||||
UNIQUE INDEX auth_userid_scheme(userid, scheme)
|
||||
);
|
||||
|
||||
|
||||
# Topics
|
||||
CREATE TABLE topics(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
touchedat DATETIME(3),
|
||||
@@ -179,27 +266,32 @@ CREATE TABLE topics(
|
||||
aux JSON,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX topics_tenant_name(tenant_id, name),
|
||||
UNIQUE INDEX topics_name (name),
|
||||
INDEX topics_owner(owner),
|
||||
INDEX topics_tenant_owner(tenant_id, owner),
|
||||
INDEX topics_state_stateat(state, stateat),
|
||||
INDEX topics_name_state_seqid ON topics(name, state, seqid)
|
||||
INDEX topics_name_state_seqid(name, state, seqid)
|
||||
);
|
||||
|
||||
# Indexed topic tags.
|
||||
CREATE TABLE topictags(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
topic CHAR(25) NOT NULL,
|
||||
tag VARCHAR(96) NOT NULL,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX topictags_tag (tag),
|
||||
INDEX topictags_tenant_tag(tenant_id, tag),
|
||||
UNIQUE INDEX topictags_topic_tag(topic, tag)
|
||||
);
|
||||
|
||||
# Subscriptions
|
||||
CREATE TABLE subscriptions(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
@@ -214,15 +306,18 @@ CREATE TABLE subscriptions(
|
||||
|
||||
PRIMARY KEY(id) ,
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
INDEX subscriptions_tenant_userid(tenant_id, userid),
|
||||
INDEX subscriptions_tenant_topic(tenant_id, topic),
|
||||
UNIQUE INDEX subscriptions_topic_userid(topic, userid),
|
||||
INDEX subscriptions_topic(topic),
|
||||
INDEX subscriptions_deletedat(deletedat),
|
||||
INDEX subscriptions_user_topic_deletedat ON subscriptions(userid, topic, deletedat)
|
||||
INDEX subscriptions_userid_topic_deletedat(userid, topic, deletedat)
|
||||
);
|
||||
|
||||
# Messages
|
||||
CREATE TABLE messages(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
@@ -230,17 +325,41 @@ CREATE TABLE messages(
|
||||
seqid INT NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
`from` BIGINT NOT NULL,
|
||||
client_msg_id VARCHAR(64),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
recalled_at DATETIME(3),
|
||||
recalled_by BIGINT,
|
||||
head JSON,
|
||||
content JSON,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX messages_tenant_topic(tenant_id, topic),
|
||||
UNIQUE INDEX messages_sender_client_mid(tenant_id, `from`, client_msg_id),
|
||||
UNIQUE INDEX messages_topic_seqid (topic, seqid)
|
||||
);
|
||||
|
||||
CREATE TABLE im_message_revisions(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version(tenant_id, topic, seqid, version),
|
||||
CONSTRAINT fk_im_message_revisions_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
# Deletion log
|
||||
CREATE TABLE dellog(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
topic CHAR(25) NOT NULL,
|
||||
deletedfor BIGINT NOT NULL DEFAULT 0,
|
||||
delid INT NOT NULL,
|
||||
@@ -249,6 +368,7 @@ CREATE TABLE dellog(
|
||||
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(topic) REFERENCES topics(name),
|
||||
INDEX dellog_tenant_topic(tenant_id, topic),
|
||||
# For getting the list of deleted message ranges
|
||||
INDEX dellog_topic_delid_deletedfor(topic,delid,deletedfor),
|
||||
# Used when getting not-yet-deleted messages(messages LEFT JOIN dellog)
|
||||
@@ -260,6 +380,7 @@ CREATE TABLE dellog(
|
||||
# User credentials
|
||||
CREATE TABLE credentials(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
deletedat DATETIME(3),
|
||||
@@ -272,13 +393,14 @@ CREATE TABLE credentials(
|
||||
retries INT NOT NULL DEFAULT 0,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE credentials_uniqueness(synthetic),
|
||||
FOREIGN KEY(userid) REFERENCES users(id),
|
||||
UNIQUE credentials_tenant_uniqueness(tenant_id, synthetic)
|
||||
);
|
||||
|
||||
# Records of uploaded files. Files themselves are stored elsewhere.
|
||||
CREATE TABLE fileuploads(
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
userid BIGINT,
|
||||
@@ -289,12 +411,14 @@ CREATE TABLE fileuploads(
|
||||
etag VARCHAR(128),
|
||||
|
||||
PRIMARY KEY(id),
|
||||
INDEX fileuploads_tenant_status(tenant_id, status),
|
||||
INDEX fileuploads_status(status)
|
||||
);
|
||||
|
||||
# Links between uploaded files and messages or topics.
|
||||
CREATE TABLE filemsglinks(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 1,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
fileid BIGINT NOT NULL,
|
||||
msgid INT,
|
||||
@@ -302,8 +426,9 @@ CREATE TABLE filemsglinks(
|
||||
userid BIGINT,
|
||||
|
||||
PRIMARY KEY(id),
|
||||
INDEX filemsglinks_tenant_fileid(tenant_id, fileid),
|
||||
FOREIGN KEY(fileid) REFERENCES fileuploads(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(topicid) REFERENCES topics(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(topic) REFERENCES topics(name) ON DELETE CASCADE,
|
||||
FOREIGN KEY(userid) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -1344,9 +1347,10 @@ func (a *adapter) TenantMessageSave(id t.TenantID, msg *t.Message) error {
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
clientMsgID := sql.NullString{String: msg.ClientMsgID, Valid: msg.ClientMsgID != ""}
|
||||
res, err := a.db.ExecContext(ctx,
|
||||
"INSERT INTO messages(tenant_id,createdat,updatedat,seqid,topic,`from`,head,content) VALUES(?,?,?,?,?,?,?,?)",
|
||||
id, msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic, decodeUID(t.ParseUid(msg.From)), msg.Head, common.ToJSON(msg.Content))
|
||||
"INSERT INTO messages(tenant_id,createdat,updatedat,seqid,topic,`from`,client_msg_id,version,head,content) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
||||
id, msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic, decodeUID(t.ParseUid(msg.From)), clientMsgID, messageVersion(msg.Version), msg.Head, common.ToJSON(msg.Content))
|
||||
if err == nil {
|
||||
lastID, _ := res.LastInsertId()
|
||||
msg.SetUid(t.Uid(lastID))
|
||||
@@ -1354,6 +1358,128 @@ func (a *adapter) TenantMessageSave(id t.TenantID, msg *t.Message) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetByClientID(id t.TenantID, from t.Uid, clientMsgID string) (*t.Message, error) {
|
||||
if err := validTenant(id); err != nil || from.IsZero() || clientMsgID == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var msg t.Message
|
||||
err := a.db.QueryRowxContext(ctx,
|
||||
"SELECT id,tenant_id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,client_msg_id,version,recalled_at,recalled_by,head,content "+
|
||||
"FROM messages WHERE tenant_id=? AND `from`=? AND client_msg_id=?",
|
||||
id, decodeUID(from), clientMsgID).StructScan(&msg)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg.From = common.EncodeUidString(msg.From).String()
|
||||
msg.Content = common.FromJSON(msg.Content)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetOne(id t.TenantID, topic string, seq int) (*t.Message, error) {
|
||||
if err := validTenant(id); err != nil || topic == "" || seq <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
msg, err := tenantMessageGetOne(a.db, ctx, id, topic, seq, "")
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageRecall(id t.TenantID, topic string, seq int, actor t.Uid, expectedVersion int, recalledAt time.Time) (*t.Message, error) {
|
||||
if err := validTenant(id); err != nil || topic == "" || seq <= 0 || actor.IsZero() || expectedVersion <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
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()
|
||||
}
|
||||
}()
|
||||
msg, err := tenantMessageGetOne(tx, ctx, id, topic, seq, " FOR UPDATE")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if msg.Version != expectedVersion || msg.RecalledAt != nil || msg.DeletedAt != nil {
|
||||
return nil, t.ErrDuplicate
|
||||
}
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE messages SET updatedat=?,version=version+1,recalled_at=?,recalled_by=?,head=NULL,content=NULL WHERE tenant_id=? AND topic=? AND seqid=? AND version=?",
|
||||
recalledAt, recalledAt, decodeUID(actor), id, topic, seq, expectedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.TenantMessageGetOne(id, topic, seq)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageEdit(id t.TenantID, topic string, seq int, actor t.Uid, expectedVersion int, head t.KVMap, content any, reason string, editedAt time.Time) (*t.Message, error) {
|
||||
if err := validTenant(id); err != nil || topic == "" || seq <= 0 || actor.IsZero() || expectedVersion <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
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()
|
||||
}
|
||||
}()
|
||||
msg, err := tenantMessageGetOne(tx, ctx, id, topic, seq, " FOR UPDATE")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if msg.Version != expectedVersion || msg.RecalledAt != nil || msg.DeletedAt != nil {
|
||||
return nil, t.ErrDuplicate
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO im_message_revisions(tenant_id,createdat,updatedat,topic,seqid,version,actor,head,content,reason) VALUES(?,?,?,?,?,?,?,?,?,?)",
|
||||
id, editedAt, editedAt, topic, seq, msg.Version, decodeUID(actor), msg.Head, common.ToJSON(msg.Content), strings.TrimSpace(reason)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE messages SET updatedat=?,version=version+1,head=?,content=? WHERE tenant_id=? AND topic=? AND seqid=? AND version=?",
|
||||
editedAt, head, common.ToJSON(content), id, topic, seq, expectedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.TenantMessageGetOne(id, topic, seq)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, opts *t.QueryOpt) ([]t.Message, error) {
|
||||
if err := validTenant(id); err != nil || topic == "" {
|
||||
return nil, t.ErrMalformed
|
||||
@@ -1390,7 +1516,7 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx,
|
||||
"SELECT m.tenant_id,m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,m.head,m.content "+
|
||||
"SELECT m.tenant_id,m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m.`from`,m.client_msg_id,m.version,m.recalled_at,m.recalled_by,m.head,m.content "+
|
||||
"FROM messages AS m LEFT JOIN dellog AS d ON d.tenant_id=m.tenant_id AND d.topic=m.topic "+
|
||||
"AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=? WHERE m.tenant_id=? AND m.delid=0 AND m.topic=? "+
|
||||
seq+" AND d.deletedfor IS NULL ORDER BY m.seqid DESC LIMIT ?",
|
||||
@@ -1412,6 +1538,34 @@ func (a *adapter) TenantMessageGetAll(id t.TenantID, topic string, uid t.Uid, op
|
||||
return msgs, rows.Err()
|
||||
}
|
||||
|
||||
func tenantMessageGetOne(q sqlx.QueryerContext, ctx context.Context, tenantID t.TenantID, topic string, seq int, suffix string) (*t.Message, error) {
|
||||
row := struct {
|
||||
t.Message
|
||||
RecalledBy sql.NullInt64 `db:"recalled_by"`
|
||||
}{}
|
||||
err := sqlx.GetContext(ctx, q, &row,
|
||||
"SELECT id,tenant_id,createdat,updatedat,deletedat,delid,seqid,topic,`from`,client_msg_id,version,recalled_at,recalled_by,head,content "+
|
||||
"FROM messages WHERE tenant_id=? AND topic=? AND seqid=?"+suffix,
|
||||
tenantID, topic, seq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := row.Message
|
||||
msg.From = common.EncodeUidString(msg.From).String()
|
||||
if row.RecalledBy.Valid {
|
||||
msg.RecalledBy = common.EncodeUidString(strconv.FormatInt(row.RecalledBy.Int64, 10)).String()
|
||||
}
|
||||
msg.Content = common.FromJSON(msg.Content)
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func messageVersion(version int) int {
|
||||
if version <= 0 {
|
||||
return 1
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMessageDeleteList(id t.TenantID, topic string, del *t.DelMessage) error {
|
||||
if err := validTenant(id); err != nil || topic == "" || (del != nil && del.TenantID != id) {
|
||||
return t.ErrMalformed
|
||||
@@ -1867,10 +2021,10 @@ func (a *adapter) TenantVerificationCreate(id t.TenantID, vc *t.VerificationCode
|
||||
vc.UpdatedAt = now
|
||||
_, err = tx.ExecContext(ctx,
|
||||
`INSERT INTO im_verification_codes
|
||||
(tenant_id,challenge_id,scene,phone_hash,phone_mask,target_user,code_hash,state,attempts,max_attempts,
|
||||
(tenant_id,challenge_id,scene,action,phone_hash,phone_mask,target_user,code_hash,state,attempts,max_attempts,
|
||||
expires_at,resend_after,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, vc.ChallengeID, vc.Scene, vc.PhoneHash, vc.PhoneMask, nullableUID(vc.TargetUser), vc.CodeHash, vc.State, vc.Attempts,
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, vc.ChallengeID, vc.Scene, vc.Action, vc.PhoneHash, vc.PhoneMask, nullableUID(vc.TargetUser), vc.CodeHash, vc.State, vc.Attempts,
|
||||
vc.MaxAttempts, vc.ExpiresAt, vc.ResendAfter, vc.CreatedAt, vc.UpdatedAt)
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
@@ -1891,7 +2045,7 @@ func (a *adapter) TenantVerificationGet(id t.TenantID, challengeID string) (*t.V
|
||||
}
|
||||
var vc t.VerificationCode
|
||||
err := a.db.GetContext(ctx, &vc,
|
||||
`SELECT id,tenant_id,challenge_id,scene,phone_hash,phone_mask,IFNULL(target_user,0) AS target_user,code_hash,verification_token_hash,
|
||||
`SELECT id,tenant_id,challenge_id,scene,action,phone_hash,phone_mask,IFNULL(target_user,0) AS target_user,code_hash,verification_token_hash,
|
||||
state,attempts,max_attempts,expires_at,resend_after,verified_at,consumed_at,created_at,updated_at
|
||||
FROM im_verification_codes
|
||||
WHERE tenant_id=? AND challenge_id=? LIMIT 1`, id, challengeID)
|
||||
@@ -1914,7 +2068,7 @@ func (a *adapter) TenantVerificationGetActive(id t.TenantID, scene, phoneHash st
|
||||
}
|
||||
var vc t.VerificationCode
|
||||
err := a.db.GetContext(ctx, &vc,
|
||||
`SELECT id,tenant_id,challenge_id,scene,phone_hash,phone_mask,IFNULL(target_user,0) AS target_user,code_hash,verification_token_hash,
|
||||
`SELECT id,tenant_id,challenge_id,scene,action,phone_hash,phone_mask,IFNULL(target_user,0) AS target_user,code_hash,verification_token_hash,
|
||||
state,attempts,max_attempts,expires_at,resend_after,verified_at,consumed_at,created_at,updated_at
|
||||
FROM im_verification_codes
|
||||
WHERE tenant_id=? AND scene=? AND phone_hash=? AND state=? AND expires_at>?
|
||||
@@ -2058,9 +2212,327 @@ func (a *adapter) TenantUserSecurityBumpAuthVersion(id t.TenantID, uid t.Uid, up
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantRealNameGet(id t.TenantID, uid t.Uid) (*t.RealNameVerification, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var rv t.RealNameVerification
|
||||
err := a.db.GetContext(ctx, &rv,
|
||||
`SELECT id,tenant_id,user_id,submission_id,status,name_cipher,id_type,id_number_cipher,id_number_hash,
|
||||
masked_name,masked_id_number,review_reason,verified_at,created_at,updated_at
|
||||
FROM im_realname_verifications
|
||||
WHERE tenant_id=? AND user_id=?
|
||||
ORDER BY created_at DESC,id DESC LIMIT 1`,
|
||||
id, decodeUID(uid))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rv, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantRealNameCreate(id t.TenantID, rv *t.RealNameVerification) error {
|
||||
if err := validTenant(id); err != nil || rv == nil || rv.TenantID != id || rv.UserID.IsZero() ||
|
||||
rv.SubmissionID == "" || rv.Status == "" || rv.NameCipher == "" || rv.IDType == "" ||
|
||||
rv.IDNumberCipher == "" || rv.IDNumberHash == "" || rv.MaskedName == "" || rv.MaskedIDNumber == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
now := t.TimeNow()
|
||||
rv.CreatedAt = now
|
||||
rv.UpdatedAt = now
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_realname_verifications
|
||||
(tenant_id,user_id,submission_id,status,name_cipher,id_type,id_number_cipher,id_number_hash,
|
||||
masked_name,masked_id_number,review_reason,verified_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, decodeUID(rv.UserID), rv.SubmissionID, rv.Status, rv.NameCipher, rv.IDType, rv.IDNumberCipher, rv.IDNumberHash,
|
||||
rv.MaskedName, rv.MaskedIDNumber, rv.ReviewReason, rv.VerifiedAt, rv.CreatedAt, rv.UpdatedAt)
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantActionTokenCreate(id t.TenantID, token *t.ActionToken) error {
|
||||
if err := validTenant(id); err != nil || token == nil || token.TenantID != id || token.UserID.IsZero() ||
|
||||
token.TokenID == "" || token.Action == "" || token.TokenHash == "" || token.ExpiresAt.IsZero() {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
now := t.TimeNow()
|
||||
token.CreatedAt = now
|
||||
token.UpdatedAt = now
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_action_tokens
|
||||
(tenant_id,user_id,token_id,action,target,token_hash,state,expires_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
||||
id, decodeUID(token.UserID), token.TokenID, token.Action, token.Target, token.TokenHash, token.State,
|
||||
token.ExpiresAt, token.CreatedAt, token.UpdatedAt)
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantActionTokenConsume(id t.TenantID, uid t.Uid, token, action, target string, now time.Time) (*t.ActionToken, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || token == "" || action == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if now.IsZero() {
|
||||
now = t.TimeNow()
|
||||
}
|
||||
tokenID, secret, ok := strings.Cut(token, ".")
|
||||
if !ok || tokenID == "" || secret == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
tokenHash := securityTokenHash(id, uid, action, target, tokenID, secret)
|
||||
ctx, cancel := a.getContextForTx()
|
||||
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()
|
||||
}
|
||||
}()
|
||||
var at t.ActionToken
|
||||
err = tx.GetContext(ctx, &at,
|
||||
`SELECT id,tenant_id,user_id,token_id,action,target,token_hash,state,expires_at,consumed_at,created_at,updated_at
|
||||
FROM im_action_tokens
|
||||
WHERE tenant_id=? AND user_id=? AND token_id=? AND action=? AND target=? LIMIT 1`,
|
||||
id, decodeUID(uid), tokenID, action, target)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if at.State != 0 || at.TokenHash != tokenHash || !now.Before(at.ExpiresAt) {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
res, err := tx.ExecContext(ctx,
|
||||
`UPDATE im_action_tokens SET state=1,consumed_at=?,updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND token_id=? AND action=? AND target=? AND state=0 AND expires_at>?`,
|
||||
now, now, id, decodeUID(uid), tokenID, action, target, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rows, _ := res.RowsAffected(); rows == 0 {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &at, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantIndependentPasswordGet(id t.TenantID, uid t.Uid, purpose string) (*t.IndependentPassword, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || purpose == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
var pass t.IndependentPassword
|
||||
err := a.db.GetContext(ctx, &pass,
|
||||
`SELECT tenant_id,user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at
|
||||
FROM im_youth_mode_settings
|
||||
WHERE tenant_id=? AND user_id=? AND purpose=?`,
|
||||
id, decodeUID(uid), purpose)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pass, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantIndependentPasswordUpsert(id t.TenantID, pass *t.IndependentPassword) error {
|
||||
if err := validTenant(id); err != nil || pass == nil || pass.TenantID != id || pass.UserID.IsZero() ||
|
||||
pass.Purpose == "" || len(pass.PasswordHash) == 0 {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
now := t.TimeNow()
|
||||
pass.CreatedAt = now
|
||||
pass.UpdatedAt = now
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_youth_mode_settings
|
||||
(tenant_id,user_id,purpose,password_hash,failed_attempts,locked_until,created_at,updated_at)
|
||||
VALUES(?,?,?,?,0,NULL,?,?)
|
||||
ON DUPLICATE KEY UPDATE password_hash=VALUES(password_hash),failed_attempts=0,locked_until=NULL,updated_at=VALUES(updated_at)`,
|
||||
id, decodeUID(pass.UserID), pass.Purpose, pass.PasswordHash, pass.CreatedAt, pass.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantIndependentPasswordUpdateAttempts(id t.TenantID, uid t.Uid, purpose string, attempts int, lockedUntil *time.Time, updatedAt time.Time) error {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || purpose == "" || attempts < 0 {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if updatedAt.IsZero() {
|
||||
updatedAt = t.TimeNow()
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`UPDATE im_youth_mode_settings SET failed_attempts=?,locked_until=?,updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND purpose=?`,
|
||||
attempts, lockedUntil, updatedAt, id, decodeUID(uid), purpose)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantSystemNotificationCreate(id t.TenantID, notification *t.SystemNotification) error {
|
||||
if err := validTenant(id); err != nil || notification == nil || notification.TenantID != id ||
|
||||
notification.UserID.IsZero() || notification.EventID == "" || notification.EventType == "" ||
|
||||
notification.Title == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
now := t.TimeNow()
|
||||
notification.CreatedAt = now
|
||||
notification.UpdatedAt = now
|
||||
_, err := a.db.ExecContext(ctx,
|
||||
`INSERT INTO im_system_notifications
|
||||
(tenant_id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
event_type=VALUES(event_type),
|
||||
title=VALUES(title),
|
||||
summary=VALUES(summary),
|
||||
action=VALUES(action),
|
||||
updated_at=VALUES(updated_at)`,
|
||||
id, decodeUID(notification.UserID), notification.EventID, notification.EventType, notification.Title,
|
||||
notification.Summary, common.ToJSON(notification.Action), notification.ReadAt, notification.CreatedAt, notification.UpdatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *adapter) TenantSystemNotificationList(id t.TenantID, uid t.Uid, cursor string, limit int) ([]t.SystemNotification, string, error) {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
lastID := int64(0)
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
if err != nil || parsed < 0 {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
lastID = parsed
|
||||
}
|
||||
query := `SELECT id,tenant_id,user_id,event_id,event_type,title,summary,action,read_at,created_at,updated_at
|
||||
FROM im_system_notifications
|
||||
WHERE tenant_id=? AND user_id=?`
|
||||
args := []any{id, decodeUID(uid)}
|
||||
if lastID > 0 {
|
||||
query += " AND id<?"
|
||||
args = append(args, lastID)
|
||||
}
|
||||
query += " ORDER BY id DESC LIMIT ?"
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
notifications := make([]t.SystemNotification, 0, limit)
|
||||
nextCursor := ""
|
||||
for rows.Next() {
|
||||
var notification t.SystemNotification
|
||||
if err = rows.StructScan(¬ification); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if action, ok := common.FromJSON(notification.Action).(map[string]any); ok {
|
||||
notification.Action = t.KVMap(action)
|
||||
}
|
||||
if len(notifications) == limit {
|
||||
nextCursor = strconv.FormatInt(notification.ID, 10)
|
||||
continue
|
||||
}
|
||||
notifications = append(notifications, notification)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return notifications, nextCursor, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantSystemNotificationMarkRead(id t.TenantID, uid t.Uid, eventID string, readAt time.Time) error {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || eventID == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if readAt.IsZero() {
|
||||
readAt = t.TimeNow()
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
res, err := a.db.ExecContext(ctx,
|
||||
`UPDATE im_system_notifications
|
||||
SET read_at=COALESCE(read_at,?),updated_at=?
|
||||
WHERE tenant_id=? AND user_id=? AND event_id=?`,
|
||||
readAt, readAt, id, decodeUID(uid), eventID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows, _ := res.RowsAffected(); rows == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nullableUID(uid t.Uid) any {
|
||||
if uid.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return store.DecodeUid(uid)
|
||||
}
|
||||
|
||||
func securityTokenHash(tenantID t.TenantID, uid t.Uid, action, target, tokenID, secret string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(strconv.FormatInt(int64(tenantID), 10)))
|
||||
h.Write([]byte{0})
|
||||
h.Write([]byte(strconv.FormatInt(store.DecodeUid(uid), 10)))
|
||||
h.Write([]byte{0})
|
||||
h.Write([]byte(action))
|
||||
h.Write([]byte{0})
|
||||
h.Write([]byte(target))
|
||||
h.Write([]byte{0})
|
||||
h.Write([]byte(tokenID))
|
||||
h.Write([]byte{0})
|
||||
h.Write([]byte(secret))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user