feat: add im auth verification flows

This commit is contained in:
richjohnrambo
2026-08-12 00:20:27 +08:00
parent 1ffcb85bee
commit d239e37f56
16 changed files with 2030 additions and 22 deletions
+232
View File
@@ -1832,3 +1832,235 @@ func (a *adapter) TenantPCacheExpire(id t.TenantID, prefix string, before time.T
}
return a.PCacheExpire(scoped, before)
}
func (a *adapter) TenantVerificationCreate(id t.TenantID, vc *t.VerificationCode) error {
if err := validTenant(id); err != nil || vc == nil || vc.TenantID != id ||
vc.ChallengeID == "" || vc.Scene == "" || vc.PhoneHash == "" || vc.CodeHash == "" {
return t.ErrMalformed
}
ctx, cancel := a.getContextForTx()
if cancel != nil {
defer cancel()
}
tx, err := a.db.BeginTxx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
now := t.TimeNow()
_, err = tx.ExecContext(ctx,
`UPDATE im_verification_codes
SET state=?,updated_at=?
WHERE tenant_id=? AND scene=? AND phone_hash=? AND state=?`,
t.VerificationStateExpired, now, id, vc.Scene, vc.PhoneHash, t.VerificationStatePending)
if err != nil {
return err
}
vc.CreatedAt = now
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,
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,
vc.MaxAttempts, vc.ExpiresAt, vc.ResendAfter, vc.CreatedAt, vc.UpdatedAt)
if isDupe(err) {
return t.ErrDuplicate
}
if err != nil {
return err
}
return tx.Commit()
}
func (a *adapter) TenantVerificationGet(id t.TenantID, challengeID string) (*t.VerificationCode, error) {
if err := validTenant(id); err != nil || challengeID == "" {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
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,
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)
if err == sql.ErrNoRows {
return nil, t.ErrNotFound
}
if err != nil {
return nil, err
}
return &vc, nil
}
func (a *adapter) TenantVerificationGetActive(id t.TenantID, scene, phoneHash string, now time.Time) (*t.VerificationCode, error) {
if err := validTenant(id); err != nil || scene == "" || phoneHash == "" {
return nil, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
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,
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>?
ORDER BY created_at DESC LIMIT 1`, id, scene, phoneHash, t.VerificationStatePending, now)
if err == sql.ErrNoRows {
return nil, t.ErrNotFound
}
if err != nil {
return nil, err
}
return &vc, nil
}
func (a *adapter) TenantVerificationFail(id t.TenantID, challengeID string, state t.VerificationState) error {
if err := validTenant(id); err != nil || challengeID == "" {
return t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
now := t.TimeNow()
_, err := a.db.ExecContext(ctx,
`UPDATE im_verification_codes
SET attempts=attempts+1,state=?,updated_at=?
WHERE tenant_id=? AND challenge_id=?`,
state, now, id, challengeID)
return err
}
func (a *adapter) TenantVerificationMarkVerified(id t.TenantID, challengeID, tokenHash string, verifiedAt time.Time) error {
if err := validTenant(id); err != nil || challengeID == "" || tokenHash == "" {
return t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
_, err := a.db.ExecContext(ctx,
`UPDATE im_verification_codes
SET state=?,verification_token_hash=?,verified_at=?,updated_at=?
WHERE tenant_id=? AND challenge_id=? AND state=?`,
t.VerificationStateVerified, tokenHash, verifiedAt, verifiedAt, id, challengeID, t.VerificationStatePending)
return err
}
func (a *adapter) TenantVerificationConsume(id t.TenantID, challengeID, tokenHash string, consumedAt time.Time) error {
if err := validTenant(id); err != nil || challengeID == "" || tokenHash == "" {
return t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
res, err := a.db.ExecContext(ctx,
`UPDATE im_verification_codes
SET state=?,consumed_at=?,updated_at=?
WHERE tenant_id=? AND challenge_id=? AND verification_token_hash=? AND state=? AND expires_at>?`,
t.VerificationStateConsumed, consumedAt, consumedAt, id, challengeID, tokenHash,
t.VerificationStateVerified, consumedAt)
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return t.ErrNotFound
}
return nil
}
func (a *adapter) TenantUserSecurityGetAuthVersion(id t.TenantID, uid t.Uid) (int, error) {
if err := validTenant(id); err != nil || uid.IsZero() {
return 0, t.ErrMalformed
}
ctx, cancel := a.getContext()
if cancel != nil {
defer cancel()
}
decoded := decodeUID(uid)
now := t.TimeNow()
_, err := a.db.ExecContext(ctx,
`INSERT INTO im_user_security(tenant_id,user_id,auth_version,created_at,updated_at)
VALUES(?,?,?,?,?)
ON DUPLICATE KEY UPDATE auth_version=auth_version`,
id, decoded, 1, now, now)
if err != nil {
return 0, err
}
var version int
err = a.db.GetContext(ctx, &version,
`SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?`,
id, decoded)
if err == sql.ErrNoRows {
return 0, t.ErrNotFound
}
if err != nil {
return 0, err
}
return version, nil
}
func (a *adapter) TenantUserSecurityBumpAuthVersion(id t.TenantID, uid t.Uid, updatedAt time.Time) (int, error) {
if err := validTenant(id); err != nil || uid.IsZero() {
return 0, t.ErrMalformed
}
if updatedAt.IsZero() {
updatedAt = t.TimeNow()
}
ctx, cancel := a.getContextForTx()
if cancel != nil {
defer cancel()
}
tx, err := a.db.BeginTxx(ctx, nil)
if err != nil {
return 0, err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
decoded := decodeUID(uid)
if _, err = tx.ExecContext(ctx,
`INSERT INTO im_user_security(tenant_id,user_id,auth_version,password_updated_at,created_at,updated_at)
VALUES(?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE
auth_version=auth_version+1,
password_updated_at=VALUES(password_updated_at),
updated_at=VALUES(updated_at)`,
id, decoded, 2, updatedAt, updatedAt, updatedAt); err != nil {
return 0, err
}
var version int
if err = tx.GetContext(ctx, &version,
`SELECT auth_version FROM im_user_security WHERE tenant_id=? AND user_id=?`,
id, decoded); err != nil {
return 0, err
}
if err = tx.Commit(); err != nil {
return 0, err
}
return version, nil
}
func nullableUID(uid t.Uid) any {
if uid.IsZero() {
return nil
}
return store.DecodeUid(uid)
}