package postgres import ( "context" "database/sql" "fmt" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/administration" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/identity" ) const SelectPasswordChangeAccountSQL = `SELECT id, phone, display_name, role, organization_id, status, password_hash, password_salt, session_version FROM public.platform_users WHERE id = $1::text FOR UPDATE` const UpdatePasswordChangeAccountSQL = `UPDATE public.platform_users SET password_hash = $2, password_salt = $3, session_version = session_version + 1, updated_at = $4 WHERE id = $1::text RETURNING id, phone, display_name, role, organization_id, status, session_version` // ChangeOwnPassword serializes password changes on the account row so two // concurrent requests cannot both verify the same previous credential. func (db *Database) ChangeOwnPassword(ctx context.Context, accountID, currentPassword, nextPassword string, now time.Time) (identity.AuthorizationSnapshot, error) { if db.config.Backend != BackendPostgres || db.transactions == nil { return identity.AuthorizationSnapshot{}, fmt.Errorf("PostgreSQL is unavailable when ZHINIAN_DATA_BACKEND=%s", db.config.Backend) } tx, err := db.transactions.Begin(ctx) if err != nil { return identity.AuthorizationSnapshot{}, fmt.Errorf("begin password change transaction: %w", err) } finished := false defer func() { if !finished { _ = tx.Rollback(ctx) } }() account, hash, salt, found, err := loadPasswordChangeAccount(ctx, tx, accountID) if err != nil { return identity.AuthorizationSnapshot{}, err } if !found || account.Status != "active" { return identity.AuthorizationSnapshot{}, &identity.PasswordChangeError{Reason: identity.PasswordChangeNotFound} } valid, err := verifyNodeScryptPassword(currentPassword, hash, salt) if err != nil { return identity.AuthorizationSnapshot{}, fmt.Errorf("verify current password: %w", err) } if !valid { return identity.AuthorizationSnapshot{}, &identity.PasswordChangeError{Reason: identity.PasswordChangeCurrentIncorrect} } password, err := administration.HashPassword(nextPassword) if err != nil { return identity.AuthorizationSnapshot{}, fmt.Errorf("hash new password: %w", err) } updated, err := updatePasswordChangeAccount(ctx, tx, accountID, password, now) if err != nil { return identity.AuthorizationSnapshot{}, err } snapshot := identity.AuthorizationSnapshot{Account: updated} if updated.OrganizationID != "" { organization, found, err := loadPasswordLoginOrganization(ctx, tx, updated.OrganizationID) if err != nil { return identity.AuthorizationSnapshot{}, err } if found { snapshot.Organization = &organization } } if err := tx.Commit(ctx); err != nil { return identity.AuthorizationSnapshot{}, fmt.Errorf("commit password change transaction: %w", err) } finished = true return snapshot, nil } func loadPasswordChangeAccount(ctx context.Context, tx Transaction, id string) (identity.AccountSnapshot, string, string, bool, error) { rows, err := tx.Query(ctx, SelectPasswordChangeAccountSQL, id) if err != nil { return identity.AccountSnapshot{}, "", "", false, fmt.Errorf("query password change account: %w", err) } defer rows.Close() if !rows.Next() { return identity.AccountSnapshot{}, "", "", false, rows.Err() } var account identity.AccountSnapshot var organizationID sql.NullString var hash, salt string if err := rows.Scan(&account.ID, &account.Phone, &account.DisplayName, &account.Role, &organizationID, &account.Status, &hash, &salt, &account.SessionVersion); err != nil { return identity.AccountSnapshot{}, "", "", false, fmt.Errorf("scan password change account: %w", err) } if organizationID.Valid { account.OrganizationID = organizationID.String } return account, hash, salt, true, rows.Err() } func updatePasswordChangeAccount(ctx context.Context, tx Transaction, id string, password administration.PasswordHash, now time.Time) (identity.AccountSnapshot, error) { rows, err := tx.Query(ctx, UpdatePasswordChangeAccountSQL, id, password.Hash, password.Salt, now) if err != nil { return identity.AccountSnapshot{}, fmt.Errorf("update password change account: %w", err) } defer rows.Close() if !rows.Next() { return identity.AccountSnapshot{}, &identity.PasswordChangeError{Reason: identity.PasswordChangeNotFound} } var account identity.AccountSnapshot var organizationID sql.NullString if err := rows.Scan(&account.ID, &account.Phone, &account.DisplayName, &account.Role, &organizationID, &account.Status, &account.SessionVersion); err != nil { return identity.AccountSnapshot{}, fmt.Errorf("scan updated password change account: %w", err) } if organizationID.Valid { account.OrganizationID = organizationID.String } return account, rows.Err() } var _ identity.PasswordChanger = (*Database)(nil)