feat: complete registration invitation and auth redesign
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user