/me віддає ім'я з бази: Principal.Username ніколи не заповнювався
All checks were successful
CI / hygiene (push) Successful in 8s
CI / web (push) Successful in 1m7s
CI / server (push) Successful in 1m36s
CI / agent (push) Successful in 57s

Перша спроба віддавала p.Username — прогін на стенді показав порожнє.
Поле оголошене в структурі й не присвоюється ніде, а в токені лежить
лише пошта. Нова UserByID читає ім'я з core.users; класти його в токен
означало б, що після перейменування людина чверть години бачить старе.

Спіймано лише тому, що перевірка йшла наживо, а не на збірці.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
byrsapty 2026-08-28 20:04:22 +03:00
parent ae07bd2a79
commit 9f901f8b8e
3 changed files with 49 additions and 1 deletions

View file

@ -7637,7 +7637,13 @@ NULL і валить запис. Правило без власних канал
**Виправлено чотири речі, кожна закриває свій бік:**
* `/me` віддає `username`. Одне поле, без якого решта марна.
* `/me` віддає `username`і читає його З БАЗИ. Перша спроба віддавала
`Principal.Username`і прогін на стенді показав порожнє: це поле
оголошене в структурі й НЕ ЗАПОВНЮЄТЬСЯ НІДЕ, а в токені лежить лише
пошта. Ще одна обіцянка без виконання — вловлена лише тому, що я
перевірив виправлення наживо, а не на збірці. Новий `UserByID`
читає ім'я з `core.users`: покласти його в токен означало б, що після
перейменування людина чверть години бачить у шапці старе ім'я.
* Вхід у профіль малюється ЗАВЖДИ для людини (не для машинного токена),
з іконкою й підписом «Мій профіль» у підказці; без імені й пошти пише
просто «Профіль» замість того, щоб зникнути.

View file

@ -250,7 +250,24 @@ func (s *Server) handleMe(w http.ResponseWriter, r *http.Request, p *Principal)
// з шапки. Разом із нею — прив'язка Telegram, без якої кнопки
// під сповіщеннями відповідають «ваш Telegram не прив'язано» і
// не дають виходу.
// Ім'я читається з бази, а не з токена: Principal.Username
// оголошене, але ніде не заповнюється, а в самому токені лежить
// лише пошта. Класти туди ще й ім'я означало б, що після
// перейменування людина чверть години бачить у шапці старе
// ім'я й не має способу це виправити.
//
// Збій запиту не валить /me: без імені інтерфейс покаже
// «Профіль», а без відповіді взагалі — викине людину на вхід.
resp["username"] = p.Username
if u, err := s.store.UserByID(r.Context(), p.UserID); err == nil {
resp["username"] = u.Username
if u.Email != "" {
resp["email"] = u.Email
}
resp["full_name"] = u.FullName
} else {
s.log.Warn("читання облікового запису для /me", "err", err)
}
} else {
resp["token_id"] = p.TokenID
resp["token_name"] = p.TokenName

View file

@ -709,3 +709,28 @@ func (s *Store) UpdateUserProfile(ctx context.Context, tenantID, userID string,
return nil
})
}
// UserByID читає обліковий запис для сторінки профілю та /me.
//
// Окремий запит, а не поле в токені доступу, і це свідомо. Токен живе
// 15 хвилин і підписаний: усе, що в нього покладено, після перейменування
// користувача лишається старим до кінця цього строку — тобто людина
// бачила б у шапці чуже, вже неіснуюче ім'я й не мала б способу це
// виправити. Ціна — один короткий SELECT на виклик, який робиться раз на
// завантаження сторінки.
func (s *Store) UserByID(ctx context.Context, userID string) (*User, error) {
var u User
err := s.bg.QueryRow(ctx, `
SELECT id::text, username, COALESCE(email,''), COALESCE(full_name,''),
COALESCE(locale,'uk'), COALESCE(timezone,'UTC'), mfa_enabled
FROM core.users WHERE id = $1
`, userID).Scan(&u.ID, &u.Username, &u.Email, &u.FullName,
&u.Locale, &u.Timezone, &u.MFA)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
return &u, nil
}