Skip to content

Commit

Permalink
feat(login): added createLoginWithMobile
Browse files Browse the repository at this point in the history
  • Loading branch information
cnlangzi committed Apr 8, 2024
1 parent e17a992 commit 56afd4f
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 10 deletions.
217 changes: 209 additions & 8 deletions auth_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (a *Auth) createLoginWithEmail(ctx context.Context, email string, passwd st
if txEmailPending {
err = txEmail.Rollback()
if err != nil {
a.logger.Error("auth: SignIn",
a.logger.Error("auth: createLoginWithEmail",
slog.Any("err", err),
slog.String("tag", "db"),
slog.String("step", "createEmail:Rollback"),
Expand All @@ -159,7 +159,7 @@ func (a *Auth) createLoginWithEmail(ctx context.Context, email string, passwd st
if txUserPending {
err = txUser.Rollback()
if err != nil {
a.logger.Error("auth: SignIn",
a.logger.Error("auth: createLoginWithEmail",
slog.Any("err", err),
slog.String("tag", "db"),
slog.String("step", "createUser:Rollback"),
Expand All @@ -171,10 +171,10 @@ func (a *Auth) createLoginWithEmail(ctx context.Context, email string, passwd st

txUser, txErr = a.db.On(id).BeginTx(ctx, &sql.TxOptions{})
if txErr != nil {
a.logger.Error("auth: SignIn",
a.logger.Error("auth: createLoginWithEmail",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "BeginTx"),
slog.String("step", "txUser:BeginTx"),
slog.Int64("user_id", id.Int64))
return u, ErrBadDatabase
}
Expand All @@ -194,7 +194,7 @@ func (a *Auth) createLoginWithEmail(ctx context.Context, email string, passwd st
// commit it first before txEmail starts. Because concurrency transaction doesn't work on SQLite
txErr = txUser.Commit()
if txErr != nil {
a.logger.Error("auth: SignIn",
a.logger.Error("auth: createLoginWithEmail",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "txtUser:Commit"),
Expand All @@ -216,10 +216,10 @@ func (a *Auth) createLoginWithEmail(ctx context.Context, email string, passwd st

txEmail, txErr = db.BeginTx(ctx, nil)
if txErr != nil {
a.logger.Error("auth: SignIn",
a.logger.Error("auth: createLoginWithEmail",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "BeginTx"),
slog.String("step", "txEmail:BeginTx"),
slog.String("email", email))
return u, ErrBadDatabase
}
Expand All @@ -232,7 +232,7 @@ func (a *Auth) createLoginWithEmail(ctx context.Context, email string, passwd st

txErr = txEmail.Commit()
if txErr != nil {
a.logger.Error("auth: SignIn",
a.logger.Error("auth: createLoginWithEmail",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "txEmail:Commit"),
Expand Down Expand Up @@ -387,3 +387,204 @@ func (a *Auth) createUserEmail(ctx context.Context, tx *sqle.Tx, userID shardid.

return nil
}

func (a *Auth) createLoginWithMobile(ctx context.Context, mobile string, passwd string, firstName, lastName string) (User, error) {
var (
txUser, txMobile *sqle.Tx
txErr error

txUserPending, txMobilePending bool
u User
)
id := a.genUser.Next()

defer func() {
// transaction fails, try to rollback
if txErr != nil {
var err error
// txEmail is not fault-tolerant, so rollback it first
if txMobilePending {
err = txMobile.Rollback()
if err != nil {
a.logger.Error("auth: createLoginWithMobile",
slog.Any("err", err),
slog.String("tag", "db"),
slog.String("step", "createMobile:Rollback"),
slog.String("mobile", mobile))
}
}

// txtUser is fault-tolerant
if txUserPending {
err = txUser.Rollback()
if err != nil {
a.logger.Error("auth: createLoginWithMobile",
slog.Any("err", err),
slog.String("tag", "db"),
slog.String("step", "createUser:Rollback"),
slog.Int64("user_id", id.Int64))
}
}
}
}()

txUser, txErr = a.db.On(id).BeginTx(ctx, &sql.TxOptions{})
if txErr != nil {
a.logger.Error("auth: createLoginWithMobile",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "BeginTx"),
slog.Int64("user_id", id.Int64))
return u, ErrBadDatabase
}
txUserPending = true

now := time.Now()
u, txErr = a.createUser(ctx, txUser, id, passwd, firstName, lastName, now)
if txErr != nil {
return u, txErr
}

_, txErr = a.createUserProfile(ctx, txUser, id, "", mobile, now)
if txErr != nil {
return u, txErr
}

// commit it first before txEmail starts. Because concurrency transaction doesn't work on SQLite
txErr = txUser.Commit()
if txErr != nil {
a.logger.Error("auth: createLoginWithMobile",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "txtUser:Commit"),
slog.String("mobile", mobile))
return u, ErrBadDatabase
}
// txUser is committed, it is impossible to rollback anymore.
// User and UserProfile have to be deleted manually when email fails to commit
txUserPending = false

h := generateHash(a.hash(), mobile, "")

var db *sqle.Context
db, txErr = a.db.OnDHT(h)

if txErr != nil {
return u, txErr
}

txMobile, txErr = db.BeginTx(ctx, nil)
if txErr != nil {
a.logger.Error("auth: createLoginWithMobile",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "txMobile:BeginTx"),
slog.String("mobile", mobile))
return u, ErrBadDatabase
}
txMobilePending = true

txErr = a.createUserMobile(ctx, txMobile, id, mobile, h, now)
if txErr != nil {
return u, txErr
}

txErr = txMobile.Commit()
if txErr != nil {
a.logger.Error("auth: createLoginWithMobile",
slog.Any("err", txErr),
slog.String("tag", "db"),
slog.String("step", "txMobile:Commit"),
slog.String("mobile", mobile))

// User/UserProfile are fault-tolerant, so ignore errcheck
a.deleteUser(ctx, id) // nolint: errcheck
a.deleteUserProfile(ctx, id) // nolint: errcheck

return u, ErrBadDatabase
}

return u, nil
}

func (a *Auth) getUserByMobile(ctx context.Context, mobile string) (User, error) {
var u User

h := generateHash(a.hash(), mobile, "")

db, err := a.db.OnDHT(h)
if err != nil {
return u, err
}

var userID shardid.ID
err = db.
QueryRowBuilder(ctx, a.createBuilder().
Select("<prefix>user_mobile", "user_id").
Where("hash = {hash}").
Param("hash", generateHash(a.hash(), mobile, ""))).
Scan(&userID)

if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return u, ErrMobileNotFound
}
a.logger.Error("auth: getUserByMobile",
slog.String("pos", "user_mobile"),
slog.String("tag", "db"),
slog.String("mobile", mobile),
slog.Any("err", err))
return u, ErrBadDatabase
}

err = a.db.On(userID).
QueryRowBuilder(ctx, a.createBuilder().
Select("<prefix>user").
Where("id = {id}").Param("id", userID)).
Bind(&u)

if err != nil {
// mobile exists, but user_id can't be found. so data should be corrupted.
if errors.Is(err, sql.ErrNoRows) {
a.logger.Error("auth: getUserBymobile",
slog.String("pos", "user"),
slog.String("tag", "db"),
slog.String("mobile", mobile),
slog.Int64("user_id", userID.Int64),
slog.Any("err", "mobile/user is corrupted"))

return u, ErrBadDatabase
}
a.logger.Error("auth: getUserByMobile",
slog.String("pos", "user"),
slog.String("tag", "db"),
slog.String("mobile", mobile),
slog.Any("err", err))
return u, ErrBadDatabase
}

return u, nil
}

func (a *Auth) createUserMobile(ctx context.Context, tx *sqle.Tx, userID shardid.ID, mobile string, hash string, now time.Time) error {

_, err := tx.ExecBuilder(ctx, a.createBuilder().
Insert("<prefix>user_mobile").
Set("user_id", userID).
Set("hash", hash).
Set("mask", masker.Mobile(mobile)).
Set("is_verified", false).
Set("created_at", now).
End())

if err != nil {
a.logger.Error("auth: createUserMobile",
slog.String("tag", "db"),
slog.Int64("user_id", userID.Int64),
slog.String("mobile", mobile),
slog.Any("err", err))
return ErrBadDatabase
}

return nil
}
29 changes: 27 additions & 2 deletions auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,33 @@ func (a *Auth) SignInWithCode(ctx context.Context, email, code string, option Lo
}

// SignInMobile sign in with mobile and password.
func (a *Auth) SignInMobile(ctx context.Context, mobile, passwd string, option LoginOption) (*Session, error) {
return nil, nil
func (a *Auth) SignInMobile(ctx context.Context, mobile, passwd string, option LoginOption) (Session, error) {
var (
s Session
u User
err error
)

u, err = a.getUserByMobile(ctx, mobile)

if err == nil {
if verifyHash(a.hash(), u.Passwd, passwd, u.Salt) {
return a.createSession(ctx, u.ID)
}

return s, ErrPasswdNotMatched
}

if option.CreateIfNotExists && errors.Is(err, ErrMobileNotFound) {
u, err = a.createLoginWithMobile(ctx, mobile, passwd, option.FirstName, option.LastName)
if err != nil {
return s, err
}

return a.createSession(ctx, u.ID)
}

return s, err
}

// SignInMobileWithCode sign in with mobile and code.
Expand Down
91 changes: 91 additions & 0 deletions auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,94 @@ func TestSignIn(t *testing.T) {
})
}
}

func TestSignInMobile(t *testing.T) {

authTest := createAuthTest("./tests_sign_in_mobile.db")

tests := []struct {
name string
setup func(r *require.Assertions) func()
mobile string
passwd string
option LoginOption
wantedErr error
assert func(r *require.Assertions, s Session)
}{
{
name: "mobile_not_found_should_not_work",
mobile: "1-222333444",
passwd: "abc123",
wantedErr: ErrMobileNotFound,
},
{
name: "create_if_not_exists_should_work",
mobile: "1-222333444",
passwd: "abc123",
option: LoginOption{CreateIfNotExists: true, FirstName: "first", LastName: "last"},
wantedErr: nil,
assert: func(r *require.Assertions, s Session) {
userID := shardid.Parse(s.UserID)
var id int64
err := authTest.db.On(userID).
QueryRowBuilder(context.Background(), authTest.createBuilder().
Select("<prefix>user_token", "user_id").
Where("hash = {hash}").
Param("hash", s.refreshTokenHash())).
Scan(&id)

r.NoError(err)

},
},
{
name: "passwd_not_matched_should_not_work",
mobile: "1-333444555",
passwd: "not_abc123",
wantedErr: ErrPasswdNotMatched,
setup: func(r *require.Assertions) func() {
_, err := authTest.createLoginWithMobile(context.Background(), "1-333444555", "abc123", "", "")

r.NoError(err)

return nil
},
},
{
name: "passwd_should_work",
mobile: "1-444555666",
passwd: "abc123",
setup: func(r *require.Assertions) func() {
_, err := authTest.createLoginWithMobile(context.Background(), "1-444555666", "abc123", "", "")

r.NoError(err)

return nil
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := require.New(t)
if test.setup != nil {
down := test.setup(r)
if down != nil {
defer down()
}
}

s, err := authTest.SignInMobile(context.TODO(), test.mobile, test.passwd, test.option)
if test.wantedErr == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, test.wantedErr)
}

if test.assert != nil {
test.assert(r, s)
}

})
}
}
Loading

0 comments on commit 56afd4f

Please sign in to comment.