feat: complete registration invitation and auth redesign
This commit is contained in:
+129
-53
@@ -239,6 +239,15 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
|
||||
return errors.New("mysql adapter failed to open tenant registry database: " + err.Error())
|
||||
}
|
||||
}
|
||||
if a.registryDB != nil || !databaseMissing {
|
||||
controlDB := a.db
|
||||
if a.registryDB != nil {
|
||||
controlDB = a.registryDB
|
||||
}
|
||||
if err = ensureIMRegistrationInviteControlTables(controlDB); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure registration invite control schema: " + err.Error())
|
||||
}
|
||||
}
|
||||
if !databaseMissing {
|
||||
if err = ensureIMGroupBusinessTables(a.db); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure group business schema: " + err.Error())
|
||||
@@ -252,6 +261,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
|
||||
if err = ensureUserIdentitySchema(a.db); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure user identity schema: " + err.Error())
|
||||
}
|
||||
if err = ensureIMRegistrationInviteUseTable(a.db); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure registration invite usage schema: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
@@ -454,8 +466,17 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
|
||||
db.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
if err = ensureIMRegistrationInviteUseTable(db); err != nil {
|
||||
db.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
registryDB := a.registryDB
|
||||
if registryDB == nil {
|
||||
registryDB = a.db
|
||||
}
|
||||
tenantAdp := &adapter{
|
||||
db: db,
|
||||
registryDB: registryDB,
|
||||
dsn: mysqlDefaultString(dsn, a.dsn),
|
||||
dbName: resolvedDBName,
|
||||
maxResults: a.maxResults,
|
||||
@@ -629,6 +650,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE im_users(
|
||||
id BIGINT NOT NULL,
|
||||
username VARCHAR(32) NOT NULL,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
state SMALLINT NOT NULL DEFAULT 0,
|
||||
@@ -640,6 +662,7 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
public JSON,
|
||||
trusted JSON,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX users_username(username),
|
||||
UNIQUE INDEX users_phone(phone),
|
||||
INDEX users_state_stateat(state, stateat),
|
||||
INDEX users_lastseen_updatedat(lastseen, updatedat)
|
||||
@@ -667,6 +690,14 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if err = createIMNotificationTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = ensureIMRegistrationInviteUseTable(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.registryDB == nil {
|
||||
if err = ensureIMRegistrationInviteControlTables(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Indexed devices. Normalized into a separate table.
|
||||
if _, err = tx.Exec(
|
||||
@@ -689,16 +720,13 @@ func (a *adapter) CreateDb(reset bool) error {
|
||||
if _, err = tx.Exec(
|
||||
`CREATE TABLE im_auth(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
uname VARCHAR(32) NOT NULL,
|
||||
userid BIGINT NOT NULL,
|
||||
scheme VARCHAR(16) NOT NULL,
|
||||
authlvl INT NOT NULL,
|
||||
secret VARCHAR(255) NOT NULL,
|
||||
expires DATETIME,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(userid) REFERENCES im_users(id),
|
||||
UNIQUE INDEX auth_uname(uname),
|
||||
UNIQUE INDEX auth_userid_scheme(userid, scheme)
|
||||
UNIQUE INDEX auth_userid(userid)
|
||||
)`); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1659,7 +1687,7 @@ 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 {
|
||||
if user == nil || strings.TrimSpace(user.Username) == "" {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
ctx, cancel := a.getContextForTx()
|
||||
@@ -1691,8 +1719,9 @@ func (a *adapter) UserCreate(user *t.User) error {
|
||||
user.Phone = canonicalPhone
|
||||
phone = canonicalPhone
|
||||
}
|
||||
if _, err = tx.Exec("INSERT INTO im_users(id,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?)",
|
||||
if _, err = tx.Exec("INSERT INTO im_users(id,username,createdat,updatedat,state,access,phone,public,trusted) VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
decoded_uid,
|
||||
user.Username,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
user.State,
|
||||
@@ -1700,6 +1729,9 @@ func (a *adapter) UserCreate(user *t.User) error {
|
||||
phone,
|
||||
common.ToJSON(user.Public),
|
||||
common.ToJSON(user.Trusted)); err != nil {
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1712,46 +1744,57 @@ func (a *adapter) UserCreate(user *t.User) error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// TenantAuthAddRecord adds an authentication record scoped to a tenant.
|
||||
func (a *adapter) TenantAuthAddRecord(tenantID t.TenantID, uid t.Uid, scheme, unique string,
|
||||
// TenantAuthAddRecord adds the user's Basic authentication record.
|
||||
func (a *adapter) TenantAuthAddRecord(tenantID t.TenantID, uid t.Uid,
|
||||
authLvl auth.Level, secret []byte, expires time.Time) error {
|
||||
tenantAdp, err := a.routedTenantAdapter(tenantID)
|
||||
if err != nil {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.AuthAddRecord(uid, scheme, unique, authLvl, secret, expires)
|
||||
return tenantAdp.TenantAuthAddRecord(tenantID, uid, authLvl, secret, expires)
|
||||
}
|
||||
return a.AuthAddRecord(uid, scheme, unique, authLvl, secret, expires)
|
||||
var username string
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
if err = a.db.GetContext(ctx, &username, "SELECT username FROM im_users WHERE id=?", store.DecodeUid(uid)); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return a.AuthAddRecord(uid, "basic", username, authLvl, secret, expires)
|
||||
}
|
||||
|
||||
// TenantAuthDelScheme deletes an authentication scheme only in the selected tenant.
|
||||
func (a *adapter) TenantAuthDelScheme(tenantID t.TenantID, uid t.Uid, scheme string) error {
|
||||
// TenantAuthDeleteRecord deletes the Basic authentication record.
|
||||
func (a *adapter) TenantAuthDeleteRecord(tenantID t.TenantID, uid t.Uid) error {
|
||||
tenantAdp, err := a.routedTenantAdapter(tenantID)
|
||||
if err != nil {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.AuthDelScheme(uid, scheme)
|
||||
return tenantAdp.TenantAuthDeleteRecord(tenantID, uid)
|
||||
}
|
||||
return a.AuthDelScheme(uid, scheme)
|
||||
return a.AuthDelScheme(uid, "basic")
|
||||
}
|
||||
|
||||
// TenantAuthUpdRecord updates an authentication record only in the selected tenant.
|
||||
func (a *adapter) TenantAuthUpdRecord(tenantID t.TenantID, uid t.Uid, scheme, unique string,
|
||||
// TenantAuthUpdateRecord updates the Basic authentication record and optional username.
|
||||
func (a *adapter) TenantAuthUpdateRecord(tenantID t.TenantID, uid t.Uid, username string,
|
||||
authLvl auth.Level, secret []byte, expires time.Time) error {
|
||||
tenantAdp, err := a.routedTenantAdapter(tenantID)
|
||||
if err != nil {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.AuthUpdRecord(uid, scheme, unique, authLvl, secret, expires)
|
||||
return tenantAdp.TenantAuthUpdateRecord(tenantID, uid, username, authLvl, secret, expires)
|
||||
}
|
||||
return a.AuthUpdRecord(uid, scheme, unique, authLvl, secret, expires)
|
||||
return a.AuthUpdRecord(uid, "basic", username, authLvl, secret, expires)
|
||||
}
|
||||
|
||||
// TenantAuthGetRecord retrieves a user's authentication record in one tenant.
|
||||
func (a *adapter) TenantAuthGetRecord(tenantID t.TenantID, uid t.Uid, scheme string) (string,
|
||||
// TenantAuthGetRecord retrieves a user's Basic authentication record.
|
||||
func (a *adapter) TenantAuthGetRecord(tenantID t.TenantID, uid t.Uid) (string,
|
||||
auth.Level, []byte, time.Time, error) {
|
||||
var expires time.Time
|
||||
tenantAdp, err := a.routedTenantAdapter(tenantID)
|
||||
@@ -1759,13 +1802,13 @@ func (a *adapter) TenantAuthGetRecord(tenantID t.TenantID, uid t.Uid, scheme str
|
||||
return "", auth.LevelNone, nil, expires, t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.AuthGetRecord(uid, scheme)
|
||||
return tenantAdp.TenantAuthGetRecord(tenantID, uid)
|
||||
}
|
||||
return a.AuthGetRecord(uid, scheme)
|
||||
return a.AuthGetRecord(uid, "basic")
|
||||
}
|
||||
|
||||
// TenantAuthGetUniqueRecord retrieves an authentication record by tenant-local login.
|
||||
func (a *adapter) TenantAuthGetUniqueRecord(tenantID t.TenantID, unique string) (t.Uid,
|
||||
// TenantAuthGetByLogin retrieves a Basic authentication record by tenant-local login.
|
||||
func (a *adapter) TenantAuthGetByLogin(tenantID t.TenantID, login string) (t.Uid,
|
||||
auth.Level, []byte, time.Time, error) {
|
||||
var expires time.Time
|
||||
tenantAdp, err := a.routedTenantAdapter(tenantID)
|
||||
@@ -1773,9 +1816,9 @@ func (a *adapter) TenantAuthGetUniqueRecord(tenantID t.TenantID, unique string)
|
||||
return t.ZeroUid, auth.LevelNone, nil, expires, t.ErrMalformed
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.AuthGetUniqueRecord(unique)
|
||||
return tenantAdp.TenantAuthGetByLogin(tenantID, login)
|
||||
}
|
||||
return a.AuthGetUniqueRecord(unique)
|
||||
return a.AuthGetUniqueRecord(login)
|
||||
}
|
||||
|
||||
// Add user's authentication record
|
||||
@@ -1791,8 +1834,17 @@ func (a *adapter) AuthAddRecord(uid t.Uid, scheme, unique string, authLvl auth.L
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
if _, err := a.db.ExecContext(ctx, "INSERT INTO im_auth(uname,userid,scheme,authLvl,secret,expires) VALUES(?,?,?,?,?,?)",
|
||||
unique, store.DecodeUid(uid), scheme, authLvl, secret, exp); err != nil {
|
||||
var storedUsername string
|
||||
if err := a.db.GetContext(ctx, &storedUsername, "SELECT username FROM im_users WHERE id=?", store.DecodeUid(uid)); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
return err
|
||||
} else if unique != storedUsername {
|
||||
return t.ErrMalformed
|
||||
}
|
||||
if _, err := a.db.ExecContext(ctx, "INSERT INTO im_auth(userid,authLvl,secret,expires) VALUES(?,?,?,?)",
|
||||
store.DecodeUid(uid), authLvl, secret, exp); err != nil {
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
@@ -1807,7 +1859,7 @@ func (a *adapter) AuthDelScheme(user t.Uid, scheme string) error {
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
_, err := a.db.ExecContext(ctx, "DELETE FROM im_auth WHERE userid=? AND scheme=?", store.DecodeUid(user), scheme)
|
||||
_, err := a.db.ExecContext(ctx, "DELETE FROM im_auth WHERE userid=?", store.DecodeUid(user))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1829,14 +1881,8 @@ func (a *adapter) AuthDelAllRecords(user t.Uid) (int, error) {
|
||||
// Update user's authentication unique, secret, auth level.
|
||||
func (a *adapter) AuthUpdRecord(uid t.Uid, scheme, unique string, authLvl auth.Level,
|
||||
secret []byte, expires time.Time) error {
|
||||
|
||||
params := []string{"authLvl=?"}
|
||||
args := []any{authLvl}
|
||||
|
||||
if unique != "" {
|
||||
params = append(params, "uname=?")
|
||||
args = append(args, unique)
|
||||
}
|
||||
if len(secret) > 0 {
|
||||
params = append(params, "secret=?")
|
||||
args = append(args, secret)
|
||||
@@ -1845,23 +1891,46 @@ func (a *adapter) AuthUpdRecord(uid t.Uid, scheme, unique string, authLvl auth.L
|
||||
params = append(params, "expires=?")
|
||||
args = append(args, expires)
|
||||
}
|
||||
args = append(args, store.DecodeUid(uid), scheme)
|
||||
decodedUID := store.DecodeUid(uid)
|
||||
args = append(args, decodedUID)
|
||||
|
||||
ctx, cancel := a.getContext()
|
||||
ctx, cancel := a.getContextForTx()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
sql := "UPDATE im_auth SET " + strings.Join(params, ",") + " WHERE userid=? AND scheme=?"
|
||||
resp, err := a.db.ExecContext(ctx, sql, args...)
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if unique != "" {
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE im_users SET username=?,updatedat=? WHERE id=?", unique, t.TimeNow(), decodedUID); err != nil {
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
query := "UPDATE im_auth SET " + strings.Join(params, ",") + " WHERE userid=?"
|
||||
resp, err := tx.ExecContext(ctx, query, args...)
|
||||
if isDupe(err) {
|
||||
return t.ErrDuplicate
|
||||
}
|
||||
|
||||
if count, _ := resp.RowsAffected(); count <= 0 {
|
||||
return t.ErrNotFound
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
if count, _ := resp.RowsAffected(); count <= 0 {
|
||||
var exists int
|
||||
if err = tx.GetContext(ctx, &exists, "SELECT COUNT(*) FROM im_auth WHERE userid=?", decodedUID); err != nil {
|
||||
return err
|
||||
}
|
||||
if exists == 0 {
|
||||
return t.ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Retrieve user's authentication record
|
||||
@@ -1869,18 +1938,18 @@ func (a *adapter) AuthGetRecord(uid t.Uid, scheme string) (string, auth.Level, [
|
||||
var expires time.Time
|
||||
|
||||
var record struct {
|
||||
Uname string
|
||||
Authlvl auth.Level
|
||||
Secret []byte
|
||||
Expires *time.Time
|
||||
Username string
|
||||
Authlvl auth.Level
|
||||
Secret []byte
|
||||
Expires *time.Time
|
||||
}
|
||||
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
if err := a.db.GetContext(ctx, &record, "SELECT uname,secret,expires,authlvl FROM im_auth WHERE userid=? AND scheme=?",
|
||||
store.DecodeUid(uid), scheme); err != nil {
|
||||
if err := a.db.GetContext(ctx, &record, `SELECT u.username,a.secret,a.expires,a.authlvl
|
||||
FROM im_auth AS a JOIN im_users AS u ON u.id=a.userid WHERE a.userid=?`, store.DecodeUid(uid)); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Nothing found - use standard error.
|
||||
err = t.ErrNotFound
|
||||
@@ -1892,7 +1961,7 @@ func (a *adapter) AuthGetRecord(uid t.Uid, scheme string) (string, auth.Level, [
|
||||
expires = *record.Expires
|
||||
}
|
||||
|
||||
return record.Uname, record.Authlvl, record.Secret, expires, nil
|
||||
return record.Username, record.Authlvl, record.Secret, expires, nil
|
||||
}
|
||||
|
||||
// Retrieve user's authentication record
|
||||
@@ -1910,7 +1979,14 @@ func (a *adapter) AuthGetUniqueRecord(unique string) (t.Uid, auth.Level, []byte,
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
if err := a.db.GetContext(ctx, &record, "SELECT userid,secret,expires,authlvl FROM im_auth WHERE uname=?", unique); err != nil {
|
||||
lookup := unique
|
||||
if len(unique) == 11 && asciiDigits(unique) {
|
||||
lookup = "+86" + unique
|
||||
}
|
||||
if err := a.db.GetContext(ctx, &record, `SELECT a.userid,a.secret,a.expires,a.authlvl
|
||||
FROM im_users AS u JOIN im_auth AS a ON a.userid=u.id
|
||||
WHERE u.username=? OR (u.phone IS NOT NULL AND u.phone=?) ORDER BY (u.username=?) DESC LIMIT 1`,
|
||||
unique, lookup, unique); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Nothing found - clear the error
|
||||
err = nil
|
||||
@@ -1932,7 +2008,7 @@ func (a *adapter) UserGet(uid t.Uid) (*t.User, error) {
|
||||
defer cancel()
|
||||
}
|
||||
var user t.User
|
||||
err := a.db.GetContext(ctx, &user, `SELECT id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||
err := a.db.GetContext(ctx, &user, `SELECT id,username,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 {
|
||||
user.SetUid(uid)
|
||||
@@ -1963,7 +2039,7 @@ func (a *adapter) UserGetAll(ids ...t.Uid) ([]t.User, error) {
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
q, uids, _ := sqlx.In(`SELECT id,createdat,updatedat,state,stateat,access,lastseen,useragent,
|
||||
q, uids, _ := sqlx.In(`SELECT id,username,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...)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user