2026-06-25 11:21:16 +08:00

162 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package auth
import (
"context"
"database/sql"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
authdomain "hyapp/services/user-service/internal/domain/auth"
invitedomain "hyapp/services/user-service/internal/domain/invite"
userdomain "hyapp/services/user-service/internal/domain/user"
identitystorage "hyapp/services/user-service/internal/storage/mysql/identity"
invitestorage "hyapp/services/user-service/internal/storage/mysql/invite"
userstorage "hyapp/services/user-service/internal/storage/mysql/user"
)
func (r *Repository) SetPassword(ctx context.Context, account authdomain.PasswordAccount) error {
// password_accounts 以 user_id 唯一v1 只允许首次设置密码。
_, err := r.db.ExecContext(ctx, `
INSERT INTO password_accounts (app_code, user_id, password_hash, hash_alg, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?)
`, appcode.Normalize(account.AppCode), account.UserID, account.PasswordHash, account.HashAlg, account.CreatedAtMs, account.UpdatedAtMs)
if err != nil {
// 唯一键冲突映射为 PASSWORD_ALREADY_SET。
return mapAuthDuplicateError(err)
}
return nil
}
// CreatePasswordUser 原子创建完整密码账号,服务快捷建号和联调账号准备。
func (r *Repository) CreatePasswordUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, account authdomain.PasswordAccount, session authdomain.Session, inviteBind invitedomain.BindCommand, maxAccountsPerDevice int32) error {
// 普通快捷账号必须和 refresh session 同事务提交,避免返回不可登录的 uid
// 全站机器人会传入空 session只写资料和密码身份不产生任何可续期登录态。
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if err := ensureRegisterDeviceQuotaTx(ctx, tx, user.AppCode, user.RegisterDeviceID, maxAccountsPerDevice); err != nil {
// 快捷建号也写 users.register_device_id事务内锁定同设备行避免绕过注册风控上限。
return err
}
if err := userstorage.InsertUserIdentity(ctx, tx, user, identity); err != nil {
// 默认短号唯一键冲突交给 service 使用新 user_id/display_user_id 重试。
return mapAuthDuplicateError(err)
}
if _, err := invitestorage.EnsurePrimaryCodeForUser(ctx, tx, user.AppCode, user.UserID, user.CreatedAtMs); err != nil {
return err
}
if _, err := invitestorage.BindRelation(ctx, tx, inviteBind); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO password_accounts (app_code, user_id, password_hash, hash_alg, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?)
`, appcode.Normalize(account.AppCode), account.UserID, account.PasswordHash, account.HashAlg, account.CreatedAtMs, account.UpdatedAtMs); err != nil {
return mapAuthDuplicateError(err)
}
if session.SessionID != "" {
if err := insertSession(ctx, tx, session); err != nil {
// session 写入失败时必须回滚用户和密码身份,否则客户端拿不到 refresh token 却占用账号。
return err
}
}
return tx.Commit()
}
// FindPasswordByDisplayUserID 通过当前 active 展示号或 held 默认短号登录别名查找用户密码身份。
func (r *Repository) FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error) {
// 密码登录前先按输入展示号触发靓号懒过期;输入 held 默认短号时也能恢复已到期靓号。
if err := identitystorage.ExpirePrettyByDisplayUserID(ctx, r.db, displayUserID, nowMs, "lazy_password_login"); err != nil {
return authdomain.PasswordAccount{}, err
}
var account authdomain.PasswordAccount
var displayUserIDKind string
var displayUserIDExpiresAtMs int64
err := r.db.QueryRowContext(ctx, `
SELECT
pa.app_code,
pa.user_id,
active_du.display_user_id,
active_du.display_user_id_kind,
COALESCE(active_du.expires_at_ms, 0),
pa.password_hash,
pa.hash_alg,
pa.created_at_ms,
pa.updated_at_ms
FROM user_display_user_ids login_du
INNER JOIN user_display_user_ids active_du
ON active_du.app_code = login_du.app_code
AND active_du.user_id = login_du.user_id
AND active_du.status = ?
AND (active_du.expires_at_ms IS NULL OR active_du.expires_at_ms = 0 OR active_du.expires_at_ms > ?)
INNER JOIN password_accounts pa
ON pa.app_code = login_du.app_code
AND pa.user_id = login_du.user_id
WHERE login_du.app_code = ?
AND login_du.display_user_id = ?
AND (
(
login_du.status = ?
AND (login_du.expires_at_ms IS NULL OR login_du.expires_at_ms = 0 OR login_du.expires_at_ms > ?)
)
OR (
login_du.status = ?
AND login_du.display_user_id_kind = ?
)
)
LIMIT 1
`, string(userdomain.DisplayUserIDStatusActive), nowMs,
appcode.FromContext(ctx), displayUserID,
string(userdomain.DisplayUserIDStatusActive), nowMs,
string(userdomain.DisplayUserIDStatusHeld), string(userdomain.DisplayUserIDKindDefault)).
Scan(&account.AppCode, &account.UserID, &account.DisplayUserID, &displayUserIDKind, &displayUserIDExpiresAtMs, &account.PasswordHash, &account.HashAlg, &account.CreatedAtMs, &account.UpdatedAtMs)
if err == sql.ErrNoRows {
// 短号不存在、处于 released/expired、没有对应 active 展示号或未设置密码都返回 not found由 service 统一映射 AUTH_FAILED。
return authdomain.PasswordAccount{}, xerr.New(xerr.NotFound, "password account not found")
}
if err != nil {
return authdomain.PasswordAccount{}, err
}
if err := r.repairPasswordLoginIdentity(ctx, account.AppCode, account.UserID, account.DisplayUserID, displayUserIDKind, displayUserIDExpiresAtMs, nowMs); err != nil {
return authdomain.PasswordAccount{}, err
}
return account, nil
}
func (r *Repository) repairPasswordLoginIdentity(ctx context.Context, appCode string, userID int64, displayUserID string, displayUserIDKind string, expiresAtMs int64, nowMs int64) error {
// 密码登录以 user_display_user_ids 的 active 行为准;同步 users 快照后,后续 freshUser 和 token 会携带同一个当前展示号。
_, err := r.db.ExecContext(ctx, `
UPDATE users
SET current_display_user_id = ?,
current_display_user_id_kind = ?,
current_display_user_id_expires_at_ms = ?,
updated_at_ms = GREATEST(updated_at_ms, ?)
WHERE app_code = ?
AND user_id = ?
AND (
current_display_user_id <> ?
OR current_display_user_id_kind <> ?
OR COALESCE(current_display_user_id_expires_at_ms, 0) <> ?
)
`, displayUserID, displayUserIDKind, nullableExpiresAt(expiresAtMs), nowMs,
appcode.Normalize(appCode), userID, displayUserID, displayUserIDKind, expiresAtMs)
return err
}
func nullableExpiresAt(value int64) any {
if value <= 0 {
return nil
}
return value
}
// CreateSession 创建新的 refresh session。