feat: complete registration invitation and auth redesign

This commit is contained in:
richjohnrambo
2026-08-19 00:30:11 +08:00
parent e063e7879e
commit 219ecadf4c
47 changed files with 9014 additions and 224 deletions
+188
View File
@@ -81,6 +81,13 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
return err
}
}
if exists, err := identityColumnExists(ctx, conn, "im_users", "username"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_users ADD COLUMN username VARCHAR(32) NULL AFTER id"); err != nil {
return err
}
}
if profilesExist {
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "email"); err != nil {
return err
@@ -94,6 +101,9 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
if err = backfillUserPhones(ctx, conn, usersScoped); err != nil {
return err
}
if err = backfillUsernames(ctx, conn, usersScoped); err != nil {
return err
}
if profilesExist {
if err = backfillProfileEmails(ctx, conn, usersScoped); err != nil {
return err
@@ -115,6 +125,25 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
return fmt.Errorf("create unique phone index: %w", err)
}
}
usernameIndex := "users_username"
usernameColumns := "username"
if usersScoped {
usernameIndex = "uk_im_users_tenant_username"
usernameColumns = "tenant_id,username"
}
if exists, err := identityIndexExists(ctx, conn, "im_users", usernameIndex); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "CREATE UNIQUE INDEX "+quoteIdent(usernameIndex)+" ON im_users("+usernameColumns+")"); err != nil {
return fmt.Errorf("create unique username index: %w", err)
}
}
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_users MODIFY COLUMN username VARCHAR(32) NOT NULL"); err != nil {
return err
}
if err = migrateBasicAuthSchema(ctx, conn, usersScoped); err != nil {
return err
}
if exists, err := identityTableExists(ctx, conn, "im_usertags"); err != nil {
return err
@@ -133,6 +162,154 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
return nil
}
func backfillUsernames(ctx context.Context, conn *sqlx.Conn, scoped bool) error {
prefix := "0 AS tenant_id,u.id AS user_id,"
join := "a.userid=u.id"
orderBy := "u.id"
if scoped {
prefix = "u.tenant_id AS tenant_id,u.id AS user_id,"
join = "a.tenant_id=u.tenant_id AND a.userid=u.id"
orderBy = "u.tenant_id,u.id"
}
authExists, err := identityTableExists(ctx, conn, "im_auth")
if err != nil {
return err
}
selectAuth := "NULL"
authJoin := ""
if authExists {
hasUname, err := identityColumnExists(ctx, conn, "im_auth", "uname")
if err != nil {
return err
}
hasScheme, err := identityColumnExists(ctx, conn, "im_auth", "scheme")
if err != nil {
return err
}
if hasUname {
selectAuth = "CASE WHEN a.uname LIKE 'basic:%' THEN SUBSTRING(a.uname,7) ELSE a.uname END"
authJoin = " LEFT JOIN im_auth AS a ON " + join
if hasScheme {
authJoin += " AND a.scheme='basic'"
}
}
}
var rows []identityValue
query := "SELECT " + prefix + "COALESCE(NULLIF(u.username,''),NULLIF(" + selectAuth + ",''),NULLIF(u.phone,''),CONCAT('reserved_',u.id)) AS value FROM im_users AS u" + authJoin + " ORDER BY " + orderBy
if err = conn.SelectContext(ctx, &rows, query); err != nil {
return err
}
seen := make(map[string]bool, len(rows))
for _, row := range rows {
value := strings.TrimSpace(row.Value)
if strings.HasPrefix(value, "+") || (len(value) == 11 && asciiDigits(value)) {
if canonical, normalizeErr := normalizeUserPhone(value); normalizeErr == nil {
value = canonical
}
}
if value == "" || len(value) > 32 || seen[identityUniqueKey(row.TenantID, value, scoped)] {
value = fmt.Sprintf("reserved_%d", row.UserID)
}
seen[identityUniqueKey(row.TenantID, value, scoped)] = true
update := "UPDATE im_users SET username=? WHERE id=?"
args := []any{value, row.UserID}
if scoped {
update = "UPDATE im_users SET username=? WHERE tenant_id=? AND id=?"
args = []any{value, row.TenantID, row.UserID}
}
if _, err = conn.ExecContext(ctx, update, args...); err != nil {
return err
}
}
return nil
}
func migrateBasicAuthSchema(ctx context.Context, conn *sqlx.Conn, scoped bool) error {
exists, err := identityTableExists(ctx, conn, "im_auth")
if err != nil || !exists {
return err
}
if hasScheme, err := identityColumnExists(ctx, conn, "im_auth", "scheme"); err != nil {
return err
} else if hasScheme {
var unsupported int
if err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM im_auth WHERE scheme<>'basic'").Scan(&unsupported); err != nil {
return err
}
if unsupported > 0 {
return fmt.Errorf("mysql: im_auth contains %d non-basic records", unsupported)
}
}
legacyComposite, err := identityIndexExists(ctx, conn, "im_auth", "auth_userid_scheme")
if err != nil {
return err
}
if legacyComposite {
// The legacy composite index may currently support the userid foreign key.
// Give the FK a replacement before dropping that index.
if exists, err = identityIndexExists(ctx, conn, "im_auth", "auth_userid_fk"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "CREATE INDEX auth_userid_fk ON im_auth(userid)"); err != nil {
return err
}
}
}
for _, index := range []string{"auth_uname", "auth_userid_scheme"} {
if exists, err = identityIndexExists(ctx, conn, "im_auth", index); err != nil {
return err
} else if exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_auth DROP INDEX "+quoteIdent(index)); err != nil {
return err
}
}
}
for _, column := range []string{"uname", "scheme"} {
if exists, err = identityColumnExists(ctx, conn, "im_auth", column); err != nil {
return err
} else if exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_auth DROP COLUMN "+quoteIdent(column)); err != nil {
return err
}
}
}
indexName := "auth_userid"
columns := "userid"
if scoped {
indexName = "auth_tenant_userid"
columns = "tenant_id,userid"
}
if exists, err = identityIndexExists(ctx, conn, "im_auth", indexName); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "CREATE UNIQUE INDEX "+quoteIdent(indexName)+" ON im_auth("+columns+")"); err != nil {
return err
}
}
if !scoped {
if exists, err = identityIndexExists(ctx, conn, "im_auth", "auth_userid_fk"); err != nil {
return err
} else if exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_auth DROP INDEX auth_userid_fk"); err != nil {
return err
}
}
}
return nil
}
func asciiDigits(value string) bool {
if value == "" {
return false
}
for i := range value {
if value[i] < '0' || value[i] > '9' {
return false
}
}
return true
}
func backfillUserPhones(ctx context.Context, conn *sqlx.Conn, scoped bool) error {
candidates, err := identityCandidates(ctx, conn, scoped, "tel")
if err != nil {
@@ -277,6 +454,17 @@ func identityCandidates(ctx context.Context, conn *sqlx.Conn, scoped bool, metho
if exists, err := identityTableExists(ctx, conn, "im_auth"); err != nil {
return nil, err
} else if exists {
hasUname, err := identityColumnExists(ctx, conn, "im_auth", "uname")
if err != nil {
return nil, err
}
hasScheme, err := identityColumnExists(ctx, conn, "im_auth", "scheme")
if err != nil {
return nil, err
}
if !hasUname || !hasScheme {
return out, nil
}
authJoin := "a.userid=u.id"
if scoped {
authJoin = "a.tenant_id=u.tenant_id AND a.userid=u.id"